MYSQL: distinct values in join - price is double counted - php

I have a report that joining 5 tables:
1 - Coupon Uses: id - coupon_id - user_id - order_id
2 - Coupon: id - code - discount_type - discount_amount
3 - Order: id - user_id - orderread_ts - order_status
4 - Payments: id - user_id - order_id - sub - tax - discount - total - payment_collected
5 - User: id - email - fname - lname
So I'm running a report that takes in several criteria: Promo name, user email, start date, end date.
Here is my query:
SELECT
`Coupon`.`code` AS Promotion,
CONCAT('',o.`id`,'') AS Order_ID,
o.`orderread_ts` AS Order_Date,
u.`email` AS Customer_Email,
CONCAT(u.`fname`, " " ,u.`lname`) AS Name,
p.`subtotal` AS Subtotal,
p.`discount` AS Discount,
p.`total` AS Total
FROM `Coupon`
LEFT JOIN `Coupon_Uses` AS cu ON cu.`coupon_id` = `Coupon`.`id`
LEFT JOIN `Order` AS o ON o.`id` = cu.`order_id`
LEFT JOIN `User` AS u ON u.`id` = o.`user_id`
LEFT JOIN `Payments` AS p ON p.`id` = o.`id`
WHERE `Coupon`.`code` = 'email10'
AND o.`orderread_ts` > 0
AND o.`order_status` = ''
AND p.`discount` > 0
AND p.`payment_collected` = '1'
GROUP BY o.`id`
I'm getting the correct results, however, if during checkout, the sale wasn't complete (bad payment, wrong cc entry, etc.) the table Coupon Uses will have two entries for the same Order_id.
Now, the order, when counted will be show incorrect information.
What I can't figure out is how to get distinct order_id from the coupon_uses table.
UPDATED FINAL QUERY
SELECT
c.`code` AS Promotion,
o.`id` AS Order_ID,
o.`orderread_ts` AS Order_Date,
u.`email` AS Customer_Email,
CONCAT(u.`fname`, " " ,u.`lname`) AS Name,
p.`subtotal` AS Subtotal,
p.`discount` AS Discount,
p.`total` AS Total
FROM `Coupon` c
INNER JOIN (SELECT DISTINCT coupon_id, user_id, order_id
FROM `Coupon_Uses`) AS cu ON cu.coupon_id = c.id
INNER JOIN `Order` AS o
ON o.`id` = cu.`order_id`
INNER JOIN `User` AS u
ON u.`id` = cu.`user_id`
INNER JOIN `Payments` AS p
ON p.`order_id` = cu.`order_id`
WHERE c.`code` = 'email10'
AND o.`orderread_ts` > 0
AND o.`order_status` = ''
AND p.`discount` > 0
AND p.`payment_collected` = '1'
First change what the I used SELECT DISTINCT in the first join to make sure i'm getting only unique values. I joined each other table on the fields from coupon_uses, and I had an error in syntax on the last join, which is now fixed.
For performance: 683 rows affected, taking 30.1ms

If I understand correctly you can have the situation where you have two records in the Coupon_Uses table which only differ in their id value. coupon_id, user_id, order_id could be the same...
If that is so, I would really suggest to look into that, as it doesn't seem right. Could you not write such a duplicate record in another table during the transaction, so you can ensure there are no such duplicates? Or could you not add a status column so the temporary record gets a Draft status, while all others are Final?
Anyway, if this is the situation, you can solve it as follows in the query:
Replace:
LEFT JOIN `Coupon_Uses` AS cu ON cu.`coupon_id` = `Coupon`.`id`
with:
LEFT JOIN (SELECT DISTINCT coupon_id, user_id, order_id
FROM `Coupon_Uses`) AS cu ON cu.`coupon_id` = `Coupon`.`id`
But this could have a performance impact, as the join can probably not benefit from using an index.

Related

Can't find the correct SQL function ( SUM/COUNT - LEFT JOIN )

My issue with SQL function, is that I often mix SUM / COUNT, or even where I can put a WHERE clause and a GROUP BY, short version, SQL is one of my flaws.
Ok, now here's my current issue, I need to make a SQL function (in my PHP Model) that get the number of order I had "today" and their total cost, but I need to separate the result by country (sorry for my english) , for example the result I want should look like this (let's say I had 20 orders today) :
U.S : 15 orders (for a total of 288$) , CANADA : 5 orders (for a total of 94$)
I tried many things, and the only two that didn"t give me an error were these two (but far from the result I need) :
SELECT sum(o.amount) AS totalmoney, p.name, p.id, p.name_clean, p.iso_code
FROM orders AS o
LEFT JOIN customers AS c ON o.id_customer = c.id
LEFT JOIN countries AS p ON c.country = p.id
WHERE DATE(o.date_create) = CURDATE()
SELECT sum(o.amount) AS totalmoney, o.date_create, p.name, COUNT(p.id) AS totalpays,
p.name_clean, p.iso_code, c.country
FROM orders AS o
LEFT JOIN customers AS c ON o.id_customer = c.id
LEFT JOIN countries AS p ON c.country = p.id
GROUP BY o.date_create
My three tables are (table name : column usefull) :
countries : id - name
customers : id - country_id
orders : id - id_customer - amount ($) - date_create
Can you please help me?
Thank you for your time.
------------- EDIT WITH THE ANSWER I USED ---------------
Thanks to everyone who participate and helped me to have a better understanding of SQL.
SELECT sum(orders.amount) total, countries.name
FROM orders
INNER JOIN customers ON orders.id_customer = customers.id
INNER JOIN countries ON countries.id = customers.country_id
WHERE orders.date_create = ?
GROUP BY customers.id
It's exactly what I needed.
You’re wanting to group by country on a certain date, therefore your group by is country, and the where is the date:
SELECT sum(orders.amount) total, countries.name
FROM orders
INNER JOIN customers ON orders.id_customer = customers.id
INNER JOIN countries ON countries.id = customers.country_id
WHERE orders.date_create = ?
GROUP BY customers.id
Inner join is appropriate here, because the joins should only produce a single row for customer and country per order.
Try with that:
-- PostgreSQL
drop table orders;
drop table countries;
drop table customers;
CREATE TABLE countries
(
id integer not null,
name varchar(200)
);
insert into countries(id, name) values (1, 'MEXICO');
insert into countries(id, name) values (2, 'US');
insert into countries(id, name) values (3, 'CANADA');
create table customers
(
id integer not null,
name varchar(200),
country integer not null
);
insert into customers(id, name, country) values (1, 'Huey',1);
insert into customers(id, name, country) values (2, 'Dewey',2);
insert into customers(id, name, country) values (3, 'Louie',3);
create table orders
(
id integer not null,
id_customer integer not null,
amount double precision,
date_create date
);
insert into orders values (1, 1, 500.0, '20200103');
insert into orders values (2, 1, 1000, '20200103');
insert into orders values (3, 2, 500, '20200103');
insert into orders values (4, 3, 500, '20200103');
insert into orders values (5, 1, 500, '20200103');
SELECT COUNT(*) AS num_orders, sum(o.amount) AS totalmoney, p.name
FROM orders AS o
INNER JOIN customers AS c ON o.id_customer = c.id
INNER JOIN countries AS p ON c.country = p.id
WHERE DATE(o.date_create) = '20200103'
group by p.name
order by num_orders desc;
you can use over whith sum and count
select sum(amount) over(partition by contry_id) as totalamount,
count(contry_id) over(partition by contry_id) as totalcount,
name, ord.id
from orders ord left join customers cus on ord.customer_id = cus.id
left join countries con on cus.contry_id = con.id
where date_create = [date] --using string as a pseudo date when testing, so change is needed
here is a db<>fiddle with some pseudo data.
this will also get the data group by created date which match the OP need more imo.
select distinct sum(amount) over(partition by contry_id,date_create) as totalamount,
count(contry_id) over(partition by contry_id,date_create) as totalcount,
name, date_create
from orders ord left join customers cus on ord.customer_id = cus.id
left join countries con on cus.contry_id = con.id
pros with window function : over is that you can have different aggregate function with different 'condition' and don't need to group by every non aggregate column which usually lead to a lot of subqueries.
This should do what you want it to do:
SELECT
c.name,
sq.totalmoney
FROM
countries as c
JOIN
(SELECT
SUM(o.amount) AS totalmoney,
c.id
FROM
(SELECT
*
FROM
orders
WHERE
DATE(date_create) = CURDATE()
) AS o
JOIN
customers AS c
ON
o.id_customer = c.id
GROUP BY c.id) AS sq
on c.id = sq.id;

mysql query to fetch all products with no orders for a user

I have a Products table and an Orders table defined in such a way that I can do JOIN query as the following to return Products with zero orders for a specific user.
This query works but its very slow.
select * from products where id not in (select product_id from orders where user_id = 1)
The question is, how to write same query better way and faster?
No need of a subquery for that:
SELECT p.product_id
FROM
Products p
LEFT JOIN Order o ON p.product_id = o.product_id AND o.user_id = #UserId
WHERE
o.order_id IS NULL -- or any other field that cannot be null on Order
EDIT: for increased performance you may want to check as well that you have indexes in place on the Order user_id column and on your ids (more likely you have them there and probably clustered indexes, both worth to check)
You should be able to do simple LEFT JOIN
SELET * FROM products
LEFT JOIN orders ON (orders.product_id=products.id and orders.user_id=1)
WHERE orders.id IS NULL;
SELECT P.*
FROM products P
LEFT JOIN (SELECT product_id from orders where user_id = 1) O
ON P.id = O.product_id
WHERE O.product_id IS NULL

Getting SUM based on quantity in MySQL?

I have this tables
ORDERS
orders_id
order_article_id
order_invoice_id
order_customer_id
order_qty
ARTICLES
articles_id
article_name
article_qty
article_price
article_amount
CUSTOMERS
customers_id
customer_name
customer_position
customer_office
And SQL Join Table
$sql="SELECT customer_name, article_name, orders_id FROM orders
LEFT JOIN articles ON order_article_id = articles_id
LEFT JOIN customers on order_customer_id = customers_id";
From this query I need to get AMOUNT.
Amount is example
USER: MICHAEL
AMOUNT: ORDER ID = order_article_id + order_qty;
Is it possible to do that in MySQL or i need some addition PHP code?
You can do it in MySQL, using a GROUP BY query and a SUM() aggregate function:
SELECT
c.customer_name,
SUM(a.article_price*a.article_quantity) AS amount
FROM
orders o LEFT JOIN articles a
ON o.order_article_id = a.articles_id
LEFT JOIN customers c
ON o.order_customer_id = c.customers_id
GROUP BY
c.customers_id,
c.customer_name
you might want to obtain the AMOUNT per order, then you need to group by also for the order_id:
GROUP BY
o.orders_id,
c.customers_id,
c.customer_name

mysql query become slow when data reached upto 100000

I build a query a month ago on a website. It was working fine. But after a month I was informed that the website become very slow to load the page.
When I search for the problem, I found that my query is executing very slow to fetch the data from mysql database. Then I check for the database and found that the 4 tables which I was using by joins, have around 216850, 167634, 64000, 931 rows respectively.
I have already have indexed that tables. So, where I'm lacking. Please help guys.
[Edit]
Table1: user_alert
Records: 216850
DB Type: InnoDB
Indexes: id(primary)
Table2: orders
Records: 167634
DB Type: InnoDB
Indexes: id(primary), order_id, customer_id
Table3: user_registration
Records: 64000 around
DB Type: InnoDB
Indexes: id(primary), email_address
Table4: cities
Records: 931
DB Type: InnoDB
Indexes: id(primary)
Query:
SELECT uas.alert_id, uas.user_id, uas.status, ur.first_name, ur.last_name, ur.email_address, o.order_id,
CASE WHEN ct.city_name IS NULL THEN uas.city_name ELSE ct.city_name END AS city_name
FROM `user_alert` uas
LEFT JOIN orders o ON o.customer_id = uas.user_id
LEFT JOIN user_registration ur ON ur.id = uas.user_id
LEFT JOIN `cities` ct ON ct.city_id = uas.city_id
WHERE uas.status = '1'
GROUP BY uas.user_id
ORDER BY uas.create_date DESC
GROUP BY is used to aggregate values up. For example if you wanted the count of orders by a user you could use COUNT(o.order_id).....GROUP BY uas.user_id. There are multiple orders for each user, but the aggregate function is just counting them here. However if you just select o.order_id when you have a GROUP BY uas.user_id it doesn't know which of the possibly many order_id values to return for that user id.
In this case it possibly doesn't matter as it looks like the order table is the only one where there is multiple rows per use. If you want the latest one you could just use MAX(o.order_id) (assuming that the order_id is assigned is order). But if you wanted the order value it becomes more difficult.
SELECT uas.alert_id, uas.user_id, uas.status, ur.first_name, ur.last_name, ur.email_address, MAX(o.order_id) AS LatestOrderId,
IFNULL(ct.city_name, uas.city_name) AS city_name
FROM `user_alert` uas
LEFT JOIN orders o ON o.customer_id = uas.user_id
LEFT JOIN user_registration ur ON ur.id = uas.user_id
LEFT JOIN `cities` ct ON ct.city_id = uas.city_id
WHERE uas.status = '1'
GROUP BY uas.user_id
ORDER BY uas.create_date DESC
If you wanted the (say) value of the latest order then it becomes more difficult.
SELECT uas.alert_id, uas.user_id, uas.status, ur.first_name, ur.last_name, ur.email_address, Sub1.MaxOrderId AS LatestOrderId, o.order_value
IFNULL(ct.city_name, uas.city_name) AS city_name
FROM `user_alert` uas
LEFT JOIN (SELECT customer_id, MAX(order_id) AS MaxOrderId FROM orders GROUP BY customer_id) Sub1 ON Sub1.customer_id = uas.user_id
LEFT OUTER JOIN orders o ON o.customer_id = Sub1.user_id AND o.order_id = Sub1.MaxOrderId
LEFT JOIN user_registration ur ON ur.id = uas.user_id
LEFT JOIN `cities` ct ON ct.city_id = uas.city_id
WHERE uas.status = '1'
ORDER BY uas.create_date DESC
Or doing a bit of a fiddle based on GROUP_CONCAT
SELECT uas.alert_id, uas.user_id, uas.status, ur.first_name, ur.last_name, ur.email_address,
SUBSTRING_INDEX(GROUP_CONCAT(o.order_id ORDER BY o.order_id DESC), ',', 1) AS LatestOrderId,
SUBSTRING_INDEX(GROUP_CONCAT(o.order_value ORDER BY o.order_id DESC), ',', 1) AS LatestOrderValue,
IFNULL(ct.city_name, uas.city_name) AS city_name
FROM `user_alert` uas
LEFT OUTER JOIN orders o ON o.customer_id = uas.user_id AND o.order_id = Sub1.MaxOrderId
LEFT JOIN user_registration ur ON ur.id = uas.user_id
LEFT JOIN `cities` ct ON ct.city_id = uas.city_id
WHERE uas.status = '1'
GROUP BY uas.user_id
ORDER BY uas.create_date DESC

Merge two rows that contain the same Id but different data into one row

I have a very tricky question for you all
Suppose I have the following tables
Table AccountType:
Id Type
1 Checking
2 Savings
Table Customer_Account:
Customer_Id Account_Type_Id
450 1
450 2
451 1
The table Customer_Account contains a list of Customers' Ids with their Account type Ids. The Account_Type_Id is a foreign key coming from the AccountType.Id.
Suppose in the Customer_Account table, a customer named Josh (id 450) can have both a checking and a savings account as shown above. I can output this customer with his id and type of account like so by having a LEFT JOIN twice on the AccountType table:
SELECT CustAccount.Customer_Id AS Id, Account1.Type AS Account_Type_1, Account2.Type AS Account_Type_2
FROM Customer_Account CustAccount
LEFT JOIN AccountType Account1
ON Account1.Id = CustAccount.Account_Type_Id
LEFT JOIN AccountType Account2
ON Account2.Id = CustAccount.Account_Type_Id
The output will be:
Id Account_Type_1 Account_Type_2
450 Checking Checking
450 Savings Savings
451 Checking Checking
What I'm trying to do is that if a customer like Josh (id 450) has both a checking and a savings account, I want to output the two rows of data above into one row like so:
Id Account_Type_1 Account_Type_2
450 Checking Savings
And also, if a customer has only one type of account (like customer with id 451 here), I want only that type of account to appear under the corresponding column like so:
Id Account_Type_1 Account_Type_2
451 Checking
Or if customer with id 451 had only a Savings account the ouput should be:
Id Account_Type_1 Account_Type_2
451 Savings
I want 'Checking' to only appear under Accoun_Type_1 and 'Savings' under Account_Type_2. If I do a GROUP BY CustAccount.Customer_Id, I get this:
Id Account_Type_1 Account_Type_2
450 Checking Checking
451 Checking Checking
Any help from any expert will be very appreciated.
Thanks.
This looks like a straight-forward application for a FULL OUTER JOIN:
SELECT COALESCE(ac1.id, ac2.id) AS id, ac1.Account_Type_1, ac2.Account_Type_2
FROM (SELECT c.Customer_ID AS Id, t.Type AS Account_Type_1
FROM Customer_Account AS c
JOIN AccountType AS t ON c.Account_Type_ID = t.ID AND t.ID = 1) AS ac1
FULL OUTER JOIN
(SELECT c.Customer_ID AS Id, t.Type AS Account_Type_2
FROM Customer_Account AS c
JOIN AccountType AS t ON c.Account_Type_ID = t.ID AND t.ID = 2) AS ac2
ON ac1.Id = ac2.Id;
If your DBMS does not support FULL OUTER JOIN but does support LEFT OUTER JOIN, then you can use:
SELECT ac0.id, ac1.Account_Type_1, ac2.Account_Type_2
FROM (SELECT DISTINCT c.Customer_ID AS Id FROM Customer_Account AS c) AS ac0
LEFT OUTER JOIN
(SELECT c.Customer_ID AS Id, t.Type AS Account_Type_1
FROM Customer_Account AS c
JOIN AccountType AS t ON c.Account_Type_ID = t.ID AND t.ID = 1) AS ac1
ON ac0.id = ac1.id
LEFT OUTER JOIN
(SELECT c.Customer_ID AS Id, t.Type AS Account_Type_2
FROM Customer_Account AS c
JOIN AccountType AS t ON c.Account_Type_ID = t.ID AND t.ID = 2) AS ac2
ON ac0.Id = ac2.Id;
The first sub-query generates the list of customer IDs that exist; the second generates the list for account type 1 (Checking); the third generates the list for account type 2 (Saving). The joins ensure that every account is properly recognized.
i think this should help you
1: Sql Select Query Question. Merge Rows or else
this should help you
Adding more conditions to your ON clauses:
ON Account1.Id = CustAccount.Account_Type_Id and Account1.Account_Type_Id = 1
ON Account2.Id = CustAccount.Account_Type_Id and Account2.Account_Type_Id = 2
will result in output that includes the account(s) held by the customer. If they only have one account then the other account type will be NULL.
Edit: Sorry, I failed to appreciate that you don't have a table of customers. You can create a temporary table with a list of distinct Customer_Id values and use that as the first table in your JOINs.
select distinct Customer_Id into #Customers
from Customer_Account
Or the much more straightforward:
select distinct C.Customer_Id,
( select 'Checking' from Customer_Account where Customer_Id = C.CustomerId and Account_type_Id = 1 ) as Account_Type_1,
( select 'Savings' from Customer_Account where Customer_Id = C.CustomerId and Account_type_Id = 2 ) as Account_Type_2,
from Customer_Account as C

Categories