Correct way to handle changeable queries in PDO? - php

I am currently building a news website that has features such as posting content, labeling the content into categories and voting.
I have gotten to the stage where I am making a home feed for my users, this will show the newest posts but I want to give my users the ability to sort by categories.
Here is my current query
SELECT p.*,
(SELECT Count(id)
FROM comments AS c
WHERE c.post = p.id) AS commentCount,
v.type AS vote_type,
(SELECT Count(id)
FROM votes AS vo
WHERE vo.post_id = p.id) AS totalVotes
FROM posts AS p
LEFT JOIN tagged AS t
ON p.id = t.post_id
LEFT JOIN votes AS v
ON v.post_id = p.id
WHERE (v.user_id = 1 OR v.user_id IS NULL)
AND t.tag_id = 7
ORDER BY timestamp DESC
LIMIT :limit OFFSET :offset
This query has the following parts of code that make it return posts that are only assigned (tagged) with the category id of 7 ( and without these two parts the query becomes the base query I normally use for newest posts)
LEFT JOIN tagged AS t
ON p.id = t.post_id
AND t.tag_id = 7
I was wondering if there is a way to have the base query without the category code and then if needed input the category code rather than have two querys? Is there a way to make a query 'dynamic'.
I want to implement other sorting features but feel creating a bunch of (slightly) different copies of each other would be inefficient.

Yes, people do this all the time. You can either dynamically set your parameters, such as in the first example below, or you can dynamically build your SQL statement. I would think best practice would be to: 1. Make sure it doesn't get too complicated. At the point that you have a query that is everything to everybody, look at building a query builder or multiple queries. 2. Combine dynamic SQL statements with dynamic parameters.
Prepare your statement:
SELECT p.*,
(SELECT Count(id)
FROM comments AS c
WHERE c.post = p.id) AS commentCount,
v.type AS vote_type,
(SELECT Count(id)
FROM votes AS vo
WHERE vo.post_id = p.id) AS totalVotes
FROM posts AS p
LEFT JOIN tagged AS t
ON p.id = t.post_id
LEFT JOIN votes AS v
ON v.post_id = p.id
WHERE (v.user_id = 1 OR v.user_id IS NULL)
AND t.tag_id = :tagid
ORDER BY timestamp DESC
LIMIT :limit OFFSET :offset
Now bind your element (pseudo code):
if( ! isset( $someValue ) ) $someValue = 7; // I am just giving it a default value for
// an example. You can do whatever.
pdo->bindParam( ':tagid', $someValue, parameterType such as PDO::PARAM_INT);
You can keep building like this, or you can break up your bigger SQL prepared statement. By way of example:
if( $categoryCodeNeeded === true ) // add another and statement to where portion of your sql statement
$select += " AND tag_id = 7 ";
Edit to answer the question in the comment:
The code that you provided: SELECT p.* is either assigned to a variable right now, or it is inside another piece of code that looks like this:
$statement = $dbHandler->prepare('SELECT p.* FROM posts p blah blah blah' );
If it's the second case, then you can change that line of code to read:
$statement = $dbHandler->prepare( $yourOwnSelectVariable );
Then, you can build your variable however you want.
$yourOwnSelectVariable = "SELECT p.* FROM posts p";
if( $categoryCodeNeeded )
$yourOwnSelectVariable += " WHERE tag_id = :tagid ";
$statement = $dbHandler->prepare( $yourOwnSelectVariable );
if( ! isset( $tagId ) ) $tagId = 7;
$statement->bindParam( ':tagid', $tagId, PDO::PARAM_INT);

Related

How to display several subcategories and their news in one query?

I have a news site and I am trying to include a block of news in it.
see the image please
I created a category call world news and added subcategories. (Travel, News, Art, Bussines)
I display them one by one, I mean I do a query for each news, that means 4 different queries like below :
$sql = "SELECT posts.post_catId,
posts.post_seo_url,
posts.post_desc,
posts.post_type,
posts.post_status,
posts.post_title,
posts.post_image_url,
categories.catId,
categories.catName,
categories.cat_seo_url
FROM posts
LEFT JOIN categories
ON posts.post_catId = categories.catId
WHERE post_catId = catId AND cat_seo_url = 'art'
AND post_status = ?
ORDER BY post_created_at DESC LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute(['1',]);
if($stmt->rowCount() > 0){
while($row = $stmt->fetch()){
//here
}
}
My question is: is there a way to display them in one query ?
Edit : I want to display 1 news from 4 specific categories with one query instead of 4.
I know I can add a new row to categories table to secify which categories can be displayed.
You 'invert' the query, selecting the categories first and then joining to the posts table, constraining it by the post_seo_url field, like so:
$sql = "SELECT
categories.catId,
categories.catName,
categories.cat_seo_url,
posts.post_catId,
posts.post_seo_url,
posts.post_desc,
posts.post_type,
posts.post_status,
posts.post_title,
posts.post_image_url,
FROM categories
JOIN posts ON posts.post_seo_url = (
SELECT p.post_seo_url FROM posts as p
WHERE categories.catId = p.post_catId
ORDER BY p.post_created_at DESC LIMIT 1
)
WHERE post_status = ?"
Here's a method using ROW_NUMBER() function:
SELECT p.*,
categories.catId,
categories.catName,
categories.cat_seo_url
FROM
(SELECT
ROW_NUMBER() OVER (PARTITION BY posts.post_catId ORDER BY posts.post_created_at DESC) AS RN,
posts.post_catId,
posts.post_seo_url,
posts.post_desc,
posts.post_type,
posts.post_status,
posts.post_title,
posts.post_image_url
FROM posts
WHERE post_status = ? ) p
JOIN categories
ON p.post_catId = categories.catId
WHERE RN=1;
I've made the query on posts table to become a subquery with addition of ROW_NUMBER() function then JOIN it with categories table and added WHERE that only return row number = 1. I've changed the LEFT JOIN to JOIN because I don't see any reason to use LEFT JOIN in this operation. However, if there is a reason, please update it in your question.
Here's a demo fiddle

Moving the workload from PHP to MySQL

I currently have two queries that are executed in order. The first query pulls a few rows from a database and the 2nd query is executed using one value from those rows. I am only interested in the rows from the 1st query where I know the 2nd query is going to return a value higher than 0. I could potentially have thousands of rows in my first table, so I rather do the filtering in MySQL and not in PHP, I would assume it is faster.
The first query is as following
SELECT
*
FROM
wp_invoices
WHERE
deleted = 0
The 2nd query is as following:
SELECT COUNT(*) FROM wp_users A
INNER JOIN wp_usermeta B ON (A.ID = B.user_id)
LEFT JOIN wp_invoices_records C ON (A.ID = C.uid
AND C.invoice_id = %d
AND C.status != 100)
WHERE (B.meta_key = 'wp_capabilities' AND CAST(B.meta_value AS CHAR) LIKE '%%\"subscriber\"%%')
AND (C.uid IS NOT NULL)
Both queries work separately, this is not the problem. Currently, the %d in the 2nd query is replaced by PHP, which inserts the wp_invoices.id value for each row.
Ideally, I would like a query that returns only the rows from wp_invoices, where the 2nd query would return a value higher than 0. At this point I am not interested in the rows of the 2nd query, only the amount of rows found.
My table structures are as following: http://sqlfiddle.com/#!9/466cb
This is what I do in PHP, currently, to handle all this. Please keep in mind that this code was written to clarify this post, I am using deprecated MySQL functions because I don't write PHP that often anymore and haven't really looked into mysqli and I haven't checked if this code actually runs. It should give you a better indication of what I am currently doing.
<?php
/* Example code. I am not using mysql_ functions in my own solution, I just
know these functions by heart. Assume a database connection is already
established */
/* Refer to this as QUERY 1 */
$result = mysql_query("SELECT * FROM wp_invoices WHERE deleted = 0");
/* Loop over all invoices (this is not an invoice 'record'. These are
the invoice definitions, not the actual instances that people received */
while($row = mysql_fetch_assoc($result)) {
/* Refer to this as QUERY 2 */
$count_result = mysql_query(sprintf("
SELECT
COUNT(*) AS count
FROM wp_users A
INNER JOIN wp_usermeta B ON (A.ID = B.user_id)
LEFT JOIN wp_invoices_records C ON (A.ID = C.uid
AND C.invoice_id = %d
AND C.status != 100)
WHERE (B.meta_key = 'wp_capabilities'
AND CAST(B.meta_value AS CHAR) LIKE '%%\"subscriber\"%%')
AND (C.uid IS NOT NULL)", $row['id']));
if(mysql_result($count_result, 0) > 0) {
/* Here I would pull all data I would need for this invoice. $row
contains all the information about the invoice, $invoice_record
will contain all the information for this particular user about
this invoice*/
$invoice_records = mysql_query(sprintf("
SELECT
A.ID AS id,
A.display_name,
C.id AS received,
DATE_FORMAT(C.received_on, '%%d-%%m-%%Y') AS received_on,
C.status,
A.user_email,
C.price,
(SELECT meta_value FROM wp_usermeta WHERE meta_key = 'first_name'
AND user_id = A.ID) AS first_name,
(SELECT meta_value FROM wp_usermeta WHERE meta_key = 'tssnvgsl'
AND user_id = A.ID) AS insertion,
(SELECT meta_value FROM wp_usermeta WHERE meta_key = 'last_name'
AND user_id = A.ID) AS last_name
FROM wp_users A
INNER JOIN wp_usermeta B ON (A.ID = B.user_id)
LEFT JOIN wp_invoices_records C ON (A.ID = C.uid
AND C.invoice_id = %d
AND C.status != 100)
WHERE (B.meta_key = 'wp_capabilities'
AND CAST(B.meta_value AS CHAR) LIKE '%%\"subscriber\"%%')
AND (C.uid IS NOT NULL)", $row['id']));
while($invoice_record = mysql_fetch_assoc($invoice_records)) {
/* Perform logic to see if this person should receive a reminder */
}
}
}
?>
What I attempt to achieve, though:
<?php
$result = mysql_query(/* here a query that pulls all rows from QUERY 1
where QUERY 2 would have returned a count higher than 2 */);
while($row = mysql_fetch_assoc($result)) {
/* Now here I should be 100% sure that every $row has at least one
"pending" invoice record, that has not yet been paid and has a
status of < 100 */
}
?>
Alright. I have been racking my brain staring at your process and I think it is starting to make sense. If I am not mistaken you should be able to eliminate both query1 and query2 and move the logic into the LEFT JOIN of your third query like such.
SELECT
A.ID AS id,
A.display_name,
C.id AS received,
DATE_FORMAT(C.received_on, '%%d-%%m-%%Y') AS received_on,
C.status,
A.user_email,
C.price,
(SELECT meta_value FROM wp_usermeta WHERE meta_key = 'first_name'
AND user_id = A.ID) AS first_name,
(SELECT meta_value FROM wp_usermeta WHERE meta_key = 'tssnvgsl'
AND user_id = A.ID) AS insertion,
(SELECT meta_value FROM wp_usermeta WHERE meta_key = 'last_name'
AND user_id = A.ID) AS last_name
FROM wp_users A
INNER JOIN wp_usermeta B ON (A.ID = B.user_id)
LEFT JOIN wp_invoices_records C ON (A.ID = C.uid
AND C.invoice_id IN (SELECT id FROM wp_invoices WHERE deleted = 0)
AND C.status != 100)
WHERE (B.meta_key = 'wp_capabilities'
AND CAST(B.meta_value AS CHAR) LIKE '%%\"subscriber\"%%')
AND (C.uid IS NOT NULL)"
This query, instead of having to be ran every time for every record and the $row['id'] added via php within your LEFT JOIN I am using MySQL IN functionality with a subselect. This gives you the ability to join only on records from wp_invoices which where deleted.

get count of posts based on count(*)

i am trying to get number of posts that i have
Here is my query
$Query="
SELECT t.*,u.*,c.*
FROM posts as t
LEFT JOIN relations as r on r.post_id = t.post_id
LEFT JOIN users as u on t.auther_id = u.auther_id
LEFT JOIN categories as c on c.cate_id = r.cate_id
GROUP BY t.post_id
";
$Query=mysql_query($Query);
$numberOfPosts=mysql_num_rows($Query);
This query is works very well
but i am trying to convert it, i want make it faster
i want use count(*) instead of t.*
because when i use t.*, it gets the full data of posts and categories
but i want to get count only, So i decided to use count(*) but i don't know how to use it with query like this
Edit
i've replaced SELECT t.*,u.*,c.* with SELECT count(t.*)
But i got mysql Error Warning: mysql_fetch_assoc(): supplied argument
Edit 2:
i am trying SELECT count(t.post_title)
I Got this results
Array ( [count(t.post_id)] => 10 )
But i have only 2 posts!
$Query="
SELECT t.*,u.*,c.*
FROM posts as t
LEFT JOIN relations as r on r.post_id = t.post_id
LEFT JOIN users as u on t.auther_id = u.auther_id
LEFT JOIN categories as c on c.cate_id = r.cate_id
GROUP BY t.post_id
";
$Query=mysql_query($Query);
$numberOfPosts=mysql_num_rows($Query);
Let's take a step back and analyze this query for a moment.
You're selecting everything from three out of four tables used in the query. The joins create some logic to limit what you select to the proper categories, authors, etc. At the end of the day you are getting a lot of data from the database, then in PHP simply asking it how many rows were returned (mysql_num_rows). Instead, what #Dagon is trying to suggest in comments, is that you have MySQL simply count the results, and return that.
Let's refactor your query:
$query = "
SELECT COUNT(t.post_id) AS qty
FROM posts as t
LEFT JOIN relations AS r ON r.post_id = t.post_id
LEFT JOIN users AS u ON t.auther_id = u.auther_id
LEFT JOIN categories AS c ON c.cate_id = r.cate_id
GROUP BY t.post_id
";
$result = mysql_query($query);
$result_row = mysql_fetch_assoc($result);
$numberOfPosts = $result_row['qty'];
(You could also use Barattlo's custom execute_scalar function to make it more readable.)
I would need to see your table structures to be of more help on how to optimize the query and get the desired results.
try doing this:
$Query="
SELECT count(t.*) as count_all
FROM posts as t
LEFT JOIN relations as r on r.post_id = t.post_id
LEFT JOIN users as u on t.auther_id = u.auther_id
LEFT JOIN categories as c on c.cate_id = r.cate_id
GROUP BY t.post_id
";
$Query=mysql_query($Query);
$numberOfPosts=mysql_num_rows($Query);
You want to do
SELECT count(t.id) AS count FROM ....
//do query with PDO or whatever you are using
$rows = mysql_fetch_assoc();
$num_rows = $rows['count'];
You should probably simply use
SELECT count(*) as postingcount FROM posts
Why?
Because you do not have a WHERE clause, so there are no restrictions. Your JOINS do not ADD more rows to the resultset, and in the end your GROUP BY merges every duplicate occurance of a post_id that might have occurred because of joining back into one row. The result should only be counted, so assuming that the real number you want to know is the number of data sets inside the table posts, you do not need any join, and doing count(*) really is a very fast operation on tables in MySQL.
Remember to check if mysql_query returns false, because then you have to check mysql_error() and see why your query has an error.

PHP/MySQL Query In a loop, how to append to just one query?

I have a query to pull all articles out of the database..
$get_articles = $db->query("
SELECT *, a.id AS id, s.type AS type FROM ".$prefix."articles a
LEFT JOIN ".$prefix."sources s ON (s.id = source_id)
WHERE a.type!='trashed' ORDER BY a.timestamp DESC LIMIT $start, $end");
Within the loop of this query, I do then do another query on the same table to find related articles to the 'title' of the article, stored as '$related_phrase'. The query within the loop is:
// get related articles to this entry
$get_related = $db->query("
SELECT *, a.id AS id, MATCH (title, description) AGAINST ('$related_phrase') AS score FROM ".$prefix."articles a
LEFT JOIN ".$prefix."sources s ON (s.id = source_id) WHERE a.type!='trashed' AND MATCH (title, description) AGAINST ('$related_phrase') AND a.id!='$articles[id]' HAVING score > 7
ORDER BY a.timestamp DESC LIMIT 0, 3");
This basically means we have a query in a loop which is causing the pages to load very slowly.
What we want to do, is bring the query from within the loop, in the main query, so it's all working within one query, if that's possible?
Any help very much appreciated!
I don't think you would gain much speed by merging the two queries.
One thing you could try is to get a list of all articles and DISTINCT searchphrases (in e.g. temptable), and then build a query to get all related articles in one single go. Lastly match up related articles with the article list.
try this:
$articles_and_related = $db->query("
SELECT *
FROM ".$prefix."articles art
LEFT JOIN (
SELECT * FROM ".$prefix."articles x
WHERE
score > 7
AND x.type != 'trashed'
AND x.id != art.id
AND MATCH(x.title, x.description) AGAINST (art.title)
LIMIT 3
) rel
LEFT JOIN ".$prefix."sources s2 ON (s2.id = rel.source_id)
LEFT JOIN ".$prefix."sources s ON (s.id = art.source_id)
WHERE
art.type!='trashed'
ORDER BY art.timestamp DESC LIMIT $start, $end");

wordpress query custom fields and category

I have a query that creates a table view and then another that queries the view. The results are extremely slow.
Here is the code:
create or replace view $view_table_name as select * from wp_2_postmeta where post_id IN (
select ID FROM wp_2_posts wposts
LEFT JOIN wp_2_term_relationships ON (wposts.ID = wp_2_term_relationships.object_id)
LEFT JOIN wp_2_term_taxonomy ON (wp_2_term_relationships.term_taxonomy_id = wp_2_term_taxonomy.term_taxonomy_id)
WHERE wp_2_term_taxonomy.taxonomy = 'category'
AND wp_2_term_taxonomy.parent = $cat || wp_2_term_taxonomy.term_id = $cat
AND wposts.post_status = 'publish'
AND wposts.post_type = 'post')
The $values have been put it in for this example that queries the view table for the results.
select distinct(ID)
FROM $view_table_name wposts
LEFT JOIN wp_2_postmeta wpostmeta
ON wposts.ID = wpostmeta.post_id
WHERE post_status = 'publish'
AND ID NOT IN (SELECT post_id
FROM wp_2_postmeta
WHERE meta_key = '$var' && meta_value = '$value1')
AND ID NOT IN (SELECT post_id
FROM wp_2_postmeta
WHERE meta_key = '$var' && meta_value = '$value2')
AND ID NOT IN (SELECT post_id
FROM wp_2_postmeta
WHERE meta_key = '$var' && meta_value = '$value3')
AND postmeta.meta_key = 'pd_form'
ORDER BY CASE wpostmeta.meta_value
WHEN '$value5' THEN 1
WHEN '$value6' THEN 2
WHEN '$value7' THEN 3
WHEN '$value8' THEN 4
WHEN '$value9' THEN 5
WHEN '$value10' THEN 6
WHEN '$value11' THEN 7
WHEN '$value11' THEN 8
END;
The main problem here is a subquery in IN condition. Instead executing the subquery and then checking in the outer table for correspondences, MySQL is known to transform the query into a correlated subquery which is executed for each row in the outer table.
The usual solution is to get rid of the subquery in the IN in favour of a JOIN.
Another problem is that you use OUTER JOIN instead of inner JOIN though you do not actually need it (MySQL is usually smart enough to optimize it when it is trivial, but anyway you should express your intention more clearly).
And one more thing. Both queries seem to be dynamically generated. Beside optimizing the query itself, one should think how not to break the calling code. That may be tricky though.
Optimizing wordpress is always an interesting challenge.

Categories