Insert a subquery count from a main SQL query - php

I have to list some customers orders in my ecommerce back-office.
I work on the php orders page
This query works fine. Take around 4 secs. :
select o.orders_id, o.customers_email_address, o.transaction_id,
o.customers_name, o.payment_method, o.date_purchased, o.last_modified,
o.currency, o.currency_value, s.orders_status_name, ot.text as
order_total
from TABLE_ORDERS o left join TABLE_ORDERS_TOTAL ot on
(o.orders_id = ot.orders_id), TABLE_ORDERS_STATUS s
where o.orders_status = s.orders_status_id
and ot.class = 'ot_total'
order by o.orders_id DESC
I try now to add the total number of orders for each customer listed.
This standalone query works fine too :
select count(o.orders_id) as total_ord
from TABLE_ORDERS o
where o.customers_email_address = '" . $orders['customers_email_address'] . "'
With $orders['customers_email_address'] extracted from the previous query.
Now the page took 9 to 10 secondes to list customers orders and number of orders per customer.
Is there a way to merge to main query and the subquery count to reduce request time ?
I tried some left join with no success.
thanks for your help

Start by fixing the JOINs. The , is archaic and the WHERE clause turns the LEFT JOIN into an INNER JOIN.
You can try calculating the total number of orders using a window function:
select o.orders_id, o.customers_email_address,
o.transaction_id,
o.customers_name, o.payment_method,
o.date_purchased, o.last_modified,
o.currency, o.currency_value, s.orders_status_name,
ot.text as order_total,
o.total_ord
from (select o.*,
count(*) over (partition by o. customers_email_address) as total_ord
from table_orders o
) o join
table_orders_total ot
on o.orders_id = ot.orders_id join
table_orders_status s
on o.orders_status = s.orders_status_id
where ot.class = 'ot_total'
order by o.orders_id desc

After digging more : MySql doest support window Function.
So with the help of Gordon Linoff I re-wrote the query that works with a count subquery.
The cons : 14 seconds ! instead of 9 seconds. Too bad, I'll have to use the 2 queries like before.
Anyway I've learn't something.
Working but too long query :
select o.orders_id, o.customers_email_address,
o.transaction_id, o.customers_name, o.payment_method,
o.date_purchased, o.last_modified,
o.currency, o.currency_value, s.orders_status_name,
ot.text as order_total,
(select count(orders_id) FROM TABLE_ORDERS WHERE customers_email_address = o.customers_email_address) as total_ord
from table_orders o join
table_orders_total ot
on o.orders_id = ot.orders_id join
table_orders_status s
on o.orders_status = s.orders_status_id
where ot.class = 'ot_total'
order by o.orders_id desc

Related

MySQL count issue

In my application (osCommerce) I had to modify a query that look like this:
SELECT sql_cache distinct p.products_image,
p.products_subimage1,
pd.products_name,
p.products_quantity,
p.products_model,
p.products_ordered,
p.products_id,
p.products_price,
p.products_date_added,
p.products_weight,
p.products_length,
p.products_width,
p.products_height,
p.products_tax_class_id,
p.products_status,
IF(s.status, s.specials_new_products_price, NULL) AS specials_new_products_price,
IF(s.status, s.specials_new_products_price, p.products_price) AS final_price
FROM products p
LEFT JOIN specials s
ON p.products_id = s.products_id
LEFT JOIN products_to_categories p2c
ON p.products_id=p2c.products_id
LEFT JOIN products_description pd
ON p.products_id=pd.products_id
INNER JOIN filter_association_products fap
ON p.products_id =fap.products_id
LEFT JOIN products_attributes pa
ON p.products_id = pa.products_id
WHERE p.products_status = '1'
AND date_sub(curdate(),INTERVAL 3000 day) <= p.products_date_added
AND find_in_set(fap.filter_id,'126, 130')
ORDER BY p.products_date_added DESC,
pd.products_name
to end like this, for accurate results:
AND fap.filter_id IN (126, 130)
GROUP BY p.products_id
HAVING COUNT(DISTINCT fap.filter_id) = 2;
The issue I have now is that there is the following query that is using this new query and giving me wrong results.
SELECT COUNT( DISTINCT p.products_id ) AS total
FROM products p
LEFT JOIN specials s ON p.products_id = s.products_id
LEFT JOIN products_to_categories p2c
ON p.products_id = p2c.products_id
LEFT JOIN products_description pd
ON p.products_id = pd.products_id
INNER JOIN filter_association_products fap ON p.products_id = fap.products_id
LEFT JOIN products_attributes pa
ON p.products_id = pa.products_id
WHERE p.products_status = '1'
AND DATE_SUB( CURDATE( ) , INTERVAL 3000 DAY ) <= p.products_date_added
AND fap.filter_id IN ( 126, 130 )
GROUP BY p.products_id
HAVING COUNT( DISTINCT fap.filter_id ) =2
ORDER BY p.products_date_added DESC , pd.products_name
Which instead of giving a result of 1 row with the count of all the product_ids in the original query, now gives a result of multiple rows (the same amount of rows as the expected total products count) with the number 1 in each of them.
The main issue seems to be the GROUP BY p.products_id
HAVING COUNT( DISTINCT fap.filter_id ) =2
Is there any way to modify the original query so that the count query will work correctly while still using AND fap.filter_id IN ( 126, 130 )?
You need to use a subquery to get the results that you want:
select count(*)
from (<old query here>) s;
You need to aggregate at the product id level to get the products that meet the original condition. Counting the number of such products requires another aggregation.
As Gordon suggested, using a subquery should generally work.
Since this query looks like a product listing query and since you are using osCommerce, you will bump into another core issue when you modify the query to use a subquery:
The count query of the split page results will fail as it does not support subqueries.
To resolve this also, you will need to modify the core code in catalog/includes/classes/split_page_results.php
Change:
$count_query = tep_db_query("select count(" . $count_string . ") as total " . substr($this->sql_query, $pos_from, ($pos_to - $pos_from)));
To:
$count_query = tep_db_query("select count(*) as total from (" . $this->sql_query . ") AS derivedtable1");
More details here: http://forums.oscommerce.com/topic/290110-class-splitpageresults/

MySQL - Operand should contain 1 column(s) with opencart

While working on a system I'm creating, I attempted to use the following query in my project:
SELECT o.order_id,
(SELECT op.price, op.quantity, op.name FROM oc_order_product op
RIGHT JOIN oc_order o ON op.order_id=o.order_id ) AS product,
CONCAT(o.firstname, ' ', o.lastname) AS customer,
(SELECT os.name FROM oc_order_status os
WHERE os.order_status_id = o.order_status_id AND os.language_id = '1') AS status,
o.total, o.currency_code, o.currency_value, o.date_added, o.date_modified
FROM `oc_order` o WHERE o.order_status_id > '0' ORDER BY o.order_id DESC LIMIT 0,20
but it warnings: Operand should contain 1 column(s), pls help me
A subquery in the SELECT list must return exactly one expression. A subquery that returns more than one expression is not valid.
The problem is the second line, that's a subquery in the SELECT list of the outer query. And there is more than one expression in the SELECT list of the subquery.
You also need to insure that any subquery in the SELECT list will return at most one row.
I suggest you consider a JOIN operation, rather than a correlated subquery, something like this:
SELECT o.order_id
, op.price AS product_price
, op.quantity AS product_quantity
, op.name AS product_name
, CONCAT(o.firstname, ' ', o.lastname) AS customer
, ( SELECT os.name
FROM oc_order_status os
WHERE os.order_status_id = o.order_status_id
AND os.language_id = '1'
LIMIT 1
) AS status
, o.total
, o.currency_code
, o.currency_value
, o.date_added
, o.date_modified
FROM oc_order o
LEFT
JOIN oc_order_product op
ON op.order_id=o.order_id
WHERE o.order_status_id > '0'
ORDER BY o.order_id DESC
LIMIT 0,20
I left the subquery to return status in the query, as a demonstration of a subquery that is valid in terms of SQL syntax. I added the LIMIT 1 clause, to insure the query doesn't throw an error at execution time, if that subquery happens to return more than one row.
(It's not clear what problem you were trying to solve by using a subquery in the SELECT list.)

MySQL: Show latest value when using GROUP BY

My table "orders" includes the date_purchased and my table "orders_products" includes the products_id for the specific order.
I want to list a specific client's all purchased products_id (not all orders!) showing the latest date_purchased for each products_id. The list should be ordered with the latest orders_id of these at the top.
The code below will show all unique products_id as I want, but the "group by" is resulting in not showing the latest orders_id or date_purchased for each products_id…
What am I missing here?
SELECT o.orders_id, o.date_purchased, op.products_id
FROM orders o, orders_products op
WHERE o.customers_id = '" . $client_id . "' and op.orders_id = o.orders_id
GROUP BY op.products_id
ORDER BY orders_id DESC
The not exists approach is often the most efficient approach for this type of query:
SELECT o.orders_id, o.date_purchased, op.products_id
FROM orders o join
orders_products op
on op.orders_id = o.orders_id
WHERE o.customers_id = '" . $client_id . "' and
not exists (select 1
from orders o2 join
orders_products op2
on op2.orders_id = o2.orders_id
where op2.products_id = op.products_id and
o.customers_id = '" . $client_id . "' and
o2.orders_id > o.orders_id
)
ORDER BY orders_id DESC;
The logic is: "Get me all rows from orders where there is no row with the same product and a larger id." This is equivalent to saying: "Get me the max row".
For best performance, you want an index on orders(products_id, orders_id).
EDIT:
There is another approach that uses subtring_index() and group_concat(). This might be the most efficient way, if the filter on customer_id is highly selective (that is, greatly reduces the number of rows).
SELECT max(o.orders_id) as orders_id,
substring_index(group_concat(o.date_purchased order by orders_id desc), ',', 1) as date_purchased,
op.products_id
FROM orders o join
orders_products op
on op.orders_id = o.orders_id
WHERE o.customers_id = '" . $client_id . "'
GROUP BY op.products_id;
Of course, if the date purchased and orders_id are both increasing, you can simplify this to using max() for both:
SELECT max(o.orders_id) as orders_id,
max(o.date_purchased) as date_purchased,
op.products_id
FROM orders o join
orders_products op
on op.orders_id = o.orders_id
WHERE o.customers_id = '" . $client_id . "'
GROUP BY op.products_id;
Using group by the result will be grouped in un-ordered way you cannot rely on that using group by will give you the latest result in each so for this you need to first get the maximum of purchase date and then join with your orders table with using additional condition in on clause
SELECT
o.orders_id,
o.date_purchased,
oo.products_id
FROM
orders o
INNER JOIN (
SELECT orders.orders_id, MAX(orders.date_purchased) date_purchased ,orders_products.products_id
FROM orders
INNER JOIN
orders_products
ON(orders_products.orders_id = orders.orders_id)
GROUP BY orders.orders_id ,orders_products.products_id
) oo
ON( oo.orders_id = o.orders_id AND oo.date_purchased=o.date_purchased)
WHERE o.customers_id = '" . $client_id . "'
ORDER BY o.orders_id DESC
This will give the latest orders per products of customer
You might try this? Cannot test it right now.
SELECT MAX(o.orders_id), MAX(o.date_purchased), op.products_id
FROM orders o, orders_products op
WHERE o.customers_id = 1 and op.orders_id = o.orders_id
GROUP BY op.products_id

Combine the results of two SQL queries

How can I combine the results of these two sql queries either in SQL or PHP .. they're all involving joins .. I would like to combine them both and sort them by orderid .. how can I do that ?
first query
$sqlstr = "SELECT op.* FROM products_to_products_extra_fields AS p
INNER JOIN orders_roster AS r ON p.products_id = r.products_id
INNER JOIN orders_products AS op ON r.orders_id = op.orders_id
INNER JOIN orders AS o on op.orders_id = o.orders_id
WHERE p.products_extra_fields_id = 14
AND p.products_extra_fields_value between '"
. tep_db_input($startdate) . "' and '"
. tep_db_input($enddate) . " 23:59:59'
AND r.roster_status != 'Removed'
AND o.payment_method = 'Institutional Billing'
AND o.orders_status < 100001
GROUP BY o.orders_id
ORDER BY DECODE(o.cc_type, '$salt') ASC";
SECOND query
$sqlstr2 = "SELECT op.* FROM products_to_products_extra_fields AS p
INNER JOIN orders_products AS op ON p.products_id = op.products_id
INNER JOIN orders AS o on op.orders_id = o.orders_id
WHERE p.products_id IN
(SELECT products_id
FROM products_to_products_extra_fields
WHERE p.products_id NOT IN
(SELECT products_id
FROM products_to_products_extra_fields
WHERE products_extra_fields_id = 14)
)
AND o.date_purchased between '"
. tep_db_input($startdate) . "' and '"
. tep_db_input($enddate) . " 23:59:59'
AND o.payment_method = 'Institutional Billing'
AND o.orders_status < 100001
GROUP BY o.orders_id
ORDER BY DECODE(o.cc_type, '$salt') ASC";
If you need them combined on the PHP end, I am going to assume you are left with an array of arrays (MySQL Rows), which you could simply loop through both sets of results and use array_push to push them into 3rd(complete) array. You could also toy around with array_merge but sometimes with multidimensional arrays the end result isnt what you expected.
http://us3.php.net/manual/en/function.array-push.php
http://us3.php.net/function.array-merge
Just make a UNION query and get the results merged on the SQL side. No PHP needed.

Showing COUNT in LEFT JOIN subquery?

I have a query that selects order info between a selected time period. I want to include a where clause that limits the order info to all orders that have only 1 order total(through out all time).
Here is what I have so far:
SELECT o.orders_id, o.customers_id, o.customers_name, o.payment_method, o.date_purchased,o.orders_status, o.shipping_status, ot.value
FROM orders as o
LEFT JOIN orders_total as ot ON o.orders_id = ot.orders_id
WHERE date_purchased between '2011-07-30' AND '2011-08-30 23:59:59'
AND ot.class = 'ot_total'
AND o.customer_service_id = ''
OR o.customer_service_id IS NULL
ORDER BY orders_id DESC
This query gives me all orders in the specified time period. I need to include a subquery(or something similar) that counts all previous(through out all time) orders(order_count) BY customers_id. Then include a 'HAVING order_count < 2' in the where clause.
Is this possible? Does this make sense?
Just add this in you where close:
AND (
SELECT COUNT(o.id)
FROM orders o2
WHERE o2.customers_id = o.customers_id
) < 2
Or if you want to return the orders count, add it in your SELECT clause, and add a HAVING clause:
SELECT o.orders_id, ..., (
SELECT COUNT(o.id)
FROM orders o2
WHERE o2.customers_id = o.customers_id
) as orders_count
...
HAVING orders_count < 2

Categories