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
Related
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
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.
I'm trying to run a query to select customer audience, but it should select the customers who didn't get an email before. The email tracking comes from another table. This is the original query:
SELECT
c.customers_firstname,
c.customers_lastname,
o.orders_id,
o.customers_id,
c.customers_email_address
FROM
orders o,
customers c,
order_status s
WHERE
o.customers_id = c.customers_id
AND o.orders_id = s.orders_id
AND o.orders_status = s.orders_status_id
ORDER BY
o.orders_id ASC
Now, I need to check another table called tracking and see if the customer already exists in that table and if so, skip it.
This is what I've tried, but it doesn't seem to work:
SELECT
c.customers_firstname,
c.customers_lastname,
o.orders_id,
o.customers_id,
c.customers_email_address
FROM
orders o,
customers c
INNER JOIN
tracking t
ON
c.customers_id = t.customers_id,
order_status s
WHERE
o.customers_id = c.customers_id
AND o.orders_id = s.orders_id
AND o.orders_status = s.orders_status_id
AND c.customers_id NOT LIKE t.customers_id
ORDER BY
o.orders_id ASC
What am I doing wrong? Or is there any way to do this better?
ADDED: I totally forgot one more important factor - tracking table has "module" column and I need results only from "contact" module. So, in other words, I need to filter out customers who already exist in the tracking table, but only if associated with contact module, not any other module.
This is equivalent to your original query:
SELECT c.customers_firstname
, c.customers_lastname
, o.orders_id
, o.customers_id
, c.customers_email_address
FROM orders o
JOIN customers c
ON c.customers_id = o.customers_id
JOIN order_status s
ON s.orders_id = o.orders_id
AND s.orders_status_id = o.orders_status
ORDER
BY o.orders_id ASC
Add an anti-join
To meet your specification, you can use an "anti-join" pattern. We can add this to the query, before the ORDER BY clause:
LEFT
JOIN tracking t
ON t.customers_id = o.customers_id
WHERE t.customers_id IS NULL
What that's going to do is find all matching rows from the tracking table, based on the customers_id. For any rows that the query doesn't find a matching row(s) in the tracking table, it will generate a dummy row from tracking which consists of all NULL values. (That's one way of describing what an OUTER JOIN does.)
The "trick" now is to throw out all the rows that matched. And we do that by checking for a NULL value of customers_id from the tracking table (in the WHERE clause). For a match, that column won't be NULL. (The equals comparison in the join predicate guarantees us that.) So we know that if we get a NULL value for t.customers_id, that there wasn't a match.
So, this query returns the specified result set:
SELECT c.customers_firstname
, c.customers_lastname
, o.orders_id
, o.customers_id
, c.customers_email_address
FROM orders o
JOIN customers c
ON c.customers_id = o.customers_id
JOIN order_status s
ON s.orders_id = o.orders_id
AND s.orders_status_id = o.orders_status
LEFT
JOIN tracking t
ON t.customers_id = o.customers_id
WHERE t.customers_id IS NULL
ORDER
BY o.orders_id ASC
Other approaches
There are other approaches, but the anti-join is frequently the best performer.
Some other options are a NOT EXISTS predicate and a NOT IN predicate. I can add those, though I expect those solutions will be provided in other answers before I get around to it.
Starting with that first query (equivalent to the query in your question), we could also use a NOT EXISTS predicate. We'd add this before the ORDER BY clause:
WHERE NOT EXISTS
( SELECT 1
FROM tracking t
WHERE t.customers_id = o.customers_id
)
To use a NOT IN predicate, again, add this before the ORDER BY clause:
WHERE o.customers_id NOT IN
( SELECT t.customers_id
FROM tracking t
WHERE t.customers_id IS NOT NULL
)
(You may have some guarantee that tracking.customers_id is not null, but in the more general case, it's important that the subquery NOT return a NULL value, so we include a WHERE clause so that we have that guaranteed.)
With appropriate indexes, the anti-join pattern usually performs better than either the NOT EXISTS or the NOT IN, but not always.
Like spencer7593 suggested you can do the antijoin but instead of
LEFT JOIN tracking t ON t.customers_id = o.customers_id
WHERE t.customers_id IS NULL
you can write easier
JOIN tracking t ON t.customers_id = o.customers_id
I have a list of venues in the Venues table, and a list of a cities/states in the Locations table. The venue is associated with an area code unique to my organization, referred to as a SOYID. The SOYID is made up of a geographical area - each row in the Locations table has a City, State, and the corresponding SOYID. Some Venues rows have a SOYID, others do not; for those that do not, I need to find the SOYID for the city and state listed. I only want to select those Venues in a specific SOYID.
This query works, however, it takes a few seconds to load; I don't think I am writing the query correctly. Currently Venues has approx 140 rows, Locations has 40,000.
$sql = "SELECT DISTINCT a.VenueID, a.Name, a.PhotoID, a.City, a.StateAbbr
FROM Venues AS a LEFT JOIN Locations AS c ON a.City = c.city
WHERE a.SOYID = '" . mysql_real_escape_string($SOYID) . "'
OR ((c.city = a.City) AND (c.state = a.StateAbbr) AND (c.SOYID = '" . mysql_real_escape_string($SOYID) . "'))
ORDER BY a.Name ASC";
Any time you reference a column from a LEFT JOINed table (c.state and c.SOYID in your specific case) in the WHERE clause, you force that join to behave like an INNER JOIN. Instead, make those tests part of the join condition:
"SELECT DISTINCT a.VenueID, a.Name, a.PhotoID, a.City, a.StateAbbr
FROM Venues AS a
LEFT JOIN Locations AS c
ON a.City = c.city
AND a.StateAbbr = c.state
AND c.SOYID = '" . mysql_real_escape_string($SOYID) . "'
WHERE a.SOYID = '" . mysql_real_escape_string($SOYID) . "'
OR c.SOYID IS NOT NULL /* LEFT JOIN found a matching row */
ORDER BY a.Name ASC"
EDIT: Based on comments, this version should allow you do remove the DISTINCT requirement:
"SELECT a.VenueID, a.Name, a.PhotoID, a.City, a.StateAbbr
FROM Venues AS a
WHERE a.SOYID = '" . mysql_real_escape_string($SOYID) . "'
OR EXISTS(SELECT NULL
FROM Locations AS c
WHERE a.City = c.city
AND a.StateAbbr = c.state
AND c.SOYID = '" . mysql_real_escape_string($SOYID) . "')
ORDER BY a.Name ASC"
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