I'm trying to do a left join over 3 tables with a where clause, and I can't get it to work.
My tables are:
sale:
sale_id (int)
terminal_id (int)
zread_id (int)
... more fields about total amounts, dates times etc
sale_payment:
payment_id (int)
sale_id (int)
payment_type (enum: 'P','W','A')
method_id (int)
payment_amount (Decimal)
sale_writeoff_method:
method_id (int)
description (varchar)
display_order (int)
Sales can be finalised (paid) in 3 different ways (hence the Enum) a physical payment - Cash, Cheque etc, a "Write Off" payment - where stock gets used up at cost price (i.e. Wastage, Giveaways etc) or Accounts - Customer credit etc
At the end of a sales period (end of day) the user does a Z-Read which collects all the transactions that occur and generates a report where the user then has to balance the cash in the drawer etc. When I'm creating the initial Z-Read object, I can collect the information I need by using the query:
SELECT m.method_id, m.description, SUM(s.sale_total) as z_total, COUNT(s.sale_id) as total_sales
FROM sale_writeoff_method m
LEFT JOIN sale_payment p ON m.method_id = p.method_id
LEFT JOIN sale s ON s.sale_id = p.sale_id
WHERE s.zread_id IS NULL
AND (p.payment_type = 'W' OR p.payment_type IS NULL)
AND (s.terminal_id = ? OR s.terminal_id IS NULL)
GROUP BY p.payment_type, m.method_id
ORDER BY m.display_order;
Once everything is balanced and finalised, all of the collected sales for all types in the sales table are flagged with the zread_id that results from inserting this object.
Now my issue is, when I need to re-create the z-read object in the future, for re-printing reports for example, I can't get all the method_id's and descriptions to show - the query I've been using is:
SELECT m.method_id, m.description, SUM(s.sale_total) as z_total, COUNT(s.sale_id) as total_sales
FROM sale_writeoff_method m
LEFT JOIN sale_payment p ON m.method_id = p.method_id
LEFT JOIN sale s ON s.sale_id = p.sale_id
WHERE s.zread_id = 1
AND (p.payment_type = 'W' OR p.payment_type IS NULL)
GROUP BY p.payment_type, m.method_id
ORDER BY m.display_order;
But it only displays methods that had sales attached for that Z-Read period. I can't use WHERE s.zread_id IS NULL because that will include all the sales that haven't been finalised yet.
Any suggestions would be greatly appreciated!
The problem is that a left join returns nulls for the joined column values where no matching row is found, but you are checking those column values in the where clause, but the where predicates are is executed after all the rows are joined, so they'll never match and your outer joins are sunk.
ie, this example query:
select *
from table1 t1
left join table2 t2 on t2.fk = t1.id
where t2.col1 = 'x'
will never return any rows that don't have corresponding rows in table2, because col1 will be null, and comparisons with null are always false, except for col1 is null.
To fix this, you need to move the tests into the ON clause, so the comparison happens while the join is being made, like this:
select *
from table1 t1
left join table2 t2 on t2.fk = t1.id and t2.col1 = 'x'
Now the left join will still return rows while matching on the key, and applying extra predicates to further refine the match.
In your case, you are doing an outer (ie left) join to sale_payment p, but testing p.payment_type = 'W' in the where clause, which won't work.
Here's the fixed query, with the tests on the left-joined tables made in the ON clause:
SELECT
m.method_id,
m.description,
SUM(s.sale_total) as z_total,
COUNT(s.sale_id) as total_sales
FROM sale_writeoff_method m
LEFT JOIN sale_payment p ON m.method_id = p.method_id AND p.payment_type = 'W'
LEFT JOIN sale s ON s.sale_id = p.sale_id AND s.terminal_id = ?
GROUP BY m.method_id, m.description
ORDER BY m.display_order;
Note that I also removed the group by p.payment_type, because you haven't selected that column, and I added group by m.description, because you have selected that.
You may need to fine tune the query, but hopefully this will be pretty close
Related
I imagine I'm missing something pretty obvious here.
I'm trying to display a list of 'bookings' where the total charges is higher than the total payments for the booking. The charges and payments are stored in separate tables linked using foreign keys.
My query so far is:
SELECT `booking`.`id`,
SUM(`booking_charge`.`amount`) AS `charges`,
SUM(`booking_payment`.`amount`) AS `payments`
FROM `booking`
LEFT JOIN `booking_charge` ON `booking`.`id` = `booking_charge`.`booking_id`
LEFT JOIN `booking_payment` ON `booking`.`id` = `booking_payment`.`booking_id`
WHERE `charges` > `payments` ///this is the incorrect part
GROUP BY `booking`.`id`
My tables look something like this:
Booking (ID)
Booking_Charge (Booking_ID, Amount)
Booking_Payment (Booking_ID, Amount)
MySQL doesn't seem to like comparing the results from these two tables, I'm not sure what I'm missing but I'm sure it's something which would be possible.
try HAVING instead of WHERE like this
SELECT `booking`.`id`,
SUM(`booking_charge`.`amount`) AS `charges`,
SUM(`booking_payment`.`amount`) AS `payments`
FROM `booking`
LEFT JOIN `booking_charge` ON `booking`.`id` = `booking_charge`.`booking_id`
LEFT JOIN `booking_payment` ON `booking`.`id` = `booking_payment`.`booking_id`
GROUP BY `booking`.`id`
HAVING `charges` > `payments`
One of the problems with the query is the cross join between rows from `_charge` and rows from `_payment`. It's a semi-Cartesian join. Each row returned from `_charge` will be matched with each row returned from `_payment`, for a given `booking_id`.
Consider a simple example:
Let's put a single row in `_charge` for $40 for a particular `booking_id`.
And put two rows into `_payment` for $20 each, for the same `booking_id`.
The query will would return total charges of $80. (= 2 x $40). If there were instead five rows in \'_payment\' for $10 each, the query would return a total charges of $200 ( = 5 x $40)
There's a couple of approaches to addressing that issue. One approach is to do the aggregation in an inline view, and return the total of the charges and payments as a single row for each booking_id, and then join those to the booking table. With at most one row per booking_id, the cross join doesn't give rise to the problem of "duplicating" rows from _charge and/or _payment.
For example:
SELECT b.id
, IFNULL(c.amt,0) AS charges
, IFNULL(p.amt,0) AS payments
FROM booking b
LEFT
JOIN ( SELECT bc.booking_id
, SUM(bc.amount) AS amt
FROM booking_charge bc
GROUP BY bc.booking_id
) c
ON c.booking_id = b.id
LEFT
JOIN ( SELECT bp.booking_id
, SUM(bp.amount) AS amt
FROM booking_payment bp
GROUP BY bp.booking_id
) p
ON p.booking_id = b.id
WHERE IFNULL(c.amt,0) > IFNULL(p.amt,0)
We could make use of a HAVING clause, in place of the WHERE.
The query in this answer is not the only way to get the result, nor is it the most efficient. There are other query patterns that will return an equivalent result.
I've been struggling with this query for two days now. I've got a user table with some values which has a relation with an order table (user can have multiple orders). This table has a relation with order_item (order can have multiple orderItems). Order_Item has a relation with invoice (order_item can have multiple invoices.
The branch and shop have a one-on-one relation with the user.
Here are the most important values of all the tables:
user:
-userId (int)
order
-orderId (int)
-userId (int)
-inserted (date)
order_item
-orderItemId (int)
-orderId (int)
invoice
-invoiceId (int)
-orderItemId (int)
-cost (double)
The foreign keys are self-explanatory here. User->Order->OrderItem->Invoice.
What I need is a query in which each row in the result represents a user with two columns representing the total sales (sum of costs) in 2014 and 2015.
So what it has to do is show each user in a row with some info from the user table (company name, e-mail etc etc) and two columns with the total costs of 2014 and one of 2015 based on the order.inserted date value.
An example would be:
Name: | E-Mail | 2014 Cost | 2015 Cost
Google | info#google.com | €50.000 | €45.000
Now I've gotten so far that I've got a result for the first sum (showing all users regardless of cost), only when I join a second time (to calculate the 2015 cost) my previous sum costs get completely screwed up.
I tried some select queries within joins but I couldnt get any query to work. It's not like I'm a complete beginner in SQL but this is too complex for me to figure out this exact moment.
This is the query I use to get the 2014 result (and as soon as I add a second join for 2015 it gets screwed up):
SELECT t.userId, SUM(i.cost),
t.companyName, t.email,
t.website, t.tel, t.priority,
b.name AS Branch, s.name AS `Shop Name`
FROM `user` AS t
LEFT JOIN branch AS b ON b.branchId = t.branchId
LEFT JOIN shop AS s ON s.shopId = t.shopId
LEFT JOIN `order` AS o ON (o.userId = t.userId AND YEAR(o.inserted) = 2014)
LEFT JOIN order_item AS oi ON oi.orderId = o.orderId
LEFT JOIN invoice AS i ON i.orderItemId = oi.orderItemId
GROUP BY t.userId
I really hope somebody can help me with this. (I'm using mySQL/innoDB in Navicat 8).
Ultimately, this is a form of pivot table you are trying to produce. Instead of joining and testing the year conditions in the joins' ON clauses, you may place the condition directly inside SUM() aggregates like:
-- If the year matches, add the cost value into the sum
-- Otherwise, add zero
SUM(CASE WHEN YEAR(o.inserted) = 2014 THEN i.cost ELSE 0 END) AS `2014 Cost`
This eliminates the need for those extra joins. When applying the GROUP BY, it should include all columns which could potentially be different per group. MySQL allows you to omit columns in SELECT from GROUP BY where most other RDBMS would result in a query compile error.
SELECT
t.userId,
-- Apply the aggregate SUM() conditionally for each year
SUM(CASE WHEN YEAR(o.inserted) = 2014 THEN i.cost ELSE 0 END) AS `2014 Cost`
SUM(CASE WHEN YEAR(o.inserted) = 2015 THEN i.cost ELSE 0 END) AS `2015 Cost`
t.companyName,
t.email,
t.website,
t.tel,
t.priority,
b.name AS Branch,
s.name AS `Shop Name`
FROM
`user` AS t
LEFT JOIN branch AS b ON b.branchId = t.branchId
LEFT JOIN shop AS s ON s.shopId = t.shopId
LEFT JOIN `order` AS o ON (o.userId = t.userId)
LEFT JOIN order_item AS oi ON oi.orderId = o.orderId
LEFT JOIN invoice AS i ON i.orderItemId = oi.orderItemId
GROUP BY
t.userId,
-- Adding remaining SELECT fields
-- though MySQL will allow these to be omitted
-- without breaking this particular query
t.companyName,
t.email,
t.website,
t.tel,
t.priority,
Branch,
`Shop Name`
I have been going round in circles now for a couple of days with this query and it's driving me nuts. I've found partial answers using max(date) and left joins but have as yet failed to achieve the right result so would be very grateful for any help.
I have 3 tables:-
inventory
id
lots of other fields...
sale_price
id
date
*ticket_type_id
*inventory_id
ticket_type
id
ticket_name
The sale_price table is updated with a date and new ticket type every time the sale price is changed. All I want to do is display:-
find the latest record in sale_price
display the inventory information related to this
display the ticket_name relating to this
The query that I have arrived at is:-
$result = "
SELECT DISTINCT inv.*, tt.ticket_name, MAX(sp.date) AS spdate
FROM sale_price sp
LEFT
JOIN ticket_type tt
ON sp.ticket_type_id = tt.id
JOIN inventory inv
ON sp.inventory_id = inv.id
GROUP
BY sp.inventory_id
";
but it's clearly not working. I would be really grateful for any help you can give and (if it's not too much to as) a bit of detail on where I'm going wrong.
many thanks in anticipation!
If you only want to know the latest record of all then you could use following query:
SELECT
s.ticket_type_id,
s.inventory_id,
t.ticket_name,
i.lots of other fields
FROM
sale_price s
INNER JOIN
inventory i
ON
s.inventory_id = i.id
INNER JOIN
ticket_type t
ON
s.ticket_type_id = t.id;
WHERE
s.date = (
SELECT
MAX(s1.date)
FROM
sale_price s1
)
If you need the latest record for every inventory item, then you've got to extend this query by using the equality of the inventory_id in the WHERE clause of the subselect:
SELECT
s.ticket_type_id,
s.inventory_id,
t.ticket_name,
i.lots of other fields
FROM
sale_price s
INNER JOIN
inventory i
ON
s.inventory_id = i.id
INNER JOIN
ticket_type t
ON
s.ticket_type_id = t.id;
WHERE
-- gives us the newest date per inventory_id
s.date = (
SELECT
MAX(s1.date)
FROM
sale_price s1
WHERE
s.inventory_id = s1.inventory_id
)
By the way, this is a correlated subquery and it could get slow.
I'm trying to write a query that will find all rows in a group EXCEPT the row with the max value. I have a query so far that finds the row with the max value, but now I need to find all the rest of the rows. Here is a SQLFiddle with the two databases and sample data. I also have the query that finds the max rows.
http://sqlfiddle.com/#!2/514d2/33
For those that don't want to use SQLFiddle...
Tablename: listings
Tablename: bids
SELECT listings.end_date, listings.user_id, listings.title,
listings.auc_fp, listings.id, listings.auc_image1
FROM listings
JOIN bids b1
ON b1.listing_id=listings.id
LEFT JOIN bids b2
ON b2.listing_id=listings.id AND b1.bid < b2.bid
WHERE b1.user_id = 1
AND b2.bid IS NULL
AND listings.end_date > NOW()
ORDER BY listings.list_ts DESC
Above is the query that find the max rows. I'm trying to join the listings table with the bids table on listings.id=bids.listing_id. Then I need to find all the rows where a user ($user or user "1") has bid on that listing. Then I need to exclude the listings where the user is has the max bid (which is what the query above does).
I originally thought I could use the query above as a subquery to exclude the listings where the user is the max bidder. But I'm not sure if that's the best way to do it and I'm not that good with subqueries.
Note that answer was revised several times based on the comments below.
SELECT
lst.end_date,
lst.title,
lst.auc_fp,
lst.id as listing_id,
lst.auc_image1,
b.user_id as bid_user_id,
b.bid as bid_amount,
maxbids.maxbid as maxbid_for_listing
FROM listings lst
INNER JOIN
(
SELECT listing_id, MAX(bid) maxbid
FROM bids b
GROUP BY listing_id
) maxbids ON lst.id = maxbids.listing_id
INNER JOIN bids maxusers ON maxusers.bid = maxbids.maxbid AND maxusers.listing_id = maxbids.listing_id
INNER JOIN bids b ON
maxbids.listing_id = b.listing_Id AND
b.bid < maxbids.maxbid AND
b.user_id <> maxusers.user_id
WHERE lst.end_date > NOW()
ORDER BY lst.list_ts DESC
I'm trying to get a product search to work properly in MySQL 5.0.88.
Basic setup: I have table A with products
A.id
A.ean
A.styleNo
A.price
A.seller (ID e.g. 123, 456)
And a table B with pricelists
B.name
B.ean
B.alt_price
B.seller
Sellers can define optional pricelists, which are matched to the user doing the search. My search more or less looks like this:
SELECT A.styleNo, A.price, B.price
FROM arts A
LEFT JOIN pricelists B ON
B.seller = A.seller
AND B.ean = A.ean
AND B.alt_price != 0
WHERE...
// pricelists
AND ( B.name = "some_name" AND B.seller = "123" ) OR ( next applicable seller ) ...
So after the LEFT JOIN, I'm including all items from a pricelist by name and seller.
This works ok as it selects both the regular price (A) and alt_price (B) and I can check for existing alt_price when displaying the results to show the correct price to the user.
However if the seller does not give an alt-price to all of his products, I'm now displaying the product with the price from A, when in fact I DON'T want to display products from this seller which do not have a pricelist entry at all (think regional assortment).
So if user X has a pricelist "abc" and seller 123 has 500 products, 200 of which are on the pricelist "abc", I only want to display the 200 products and not 500 with 200 in the correct price.
I tried to use B.alt_price != 0 in the LEFT JOIN, but this doesn't help, because all items on there have a price.
Question
Is there a way to do this in the actual search or do I have to do it in the results loop, which I'm not really keen on doing.
SELECT
b.styleNo, b.price, a.alt_price
FROM
pricelists a
INNER JOIN
arts b ON a.seller = b.seller AND a.ean = b.ean
WHERE
a.alt_price <> 0 AND
a.name = 'name' AND
a.seller = 123
What the INNER JOIN is doing here is returning the row only if the seller and ean fields match in both tables, so it will only retrieve the products which are on the pricelist filtered through on the WHERE.
A LEFT JOIN on the other hand, will return all rows regardless of whether or not there's a match in the other table. If there is a match, the corresponding values in the second table are shown, but if there isn't, the values will be NULL from the second table, while still retaining the row data from the first table.
So if we instead did FROM arts a LEFT JOIN pricelists b ON ..., we would get all rows from the products table regardless of whether there's a match in the pricelist table. If a pricelist didn't match up, the product still shows, but with the pricelist data containing NULL values.
Note that the table on the left side of the LEFT JOIN has its row data retained regardless of matches in the table on the right side of the LEFT JOIN... hence "LEFT".
You might want to take a look at Jeff Atwood's visual explanation of joins to understand how the different JOIN types work.
Also understand that WHERE is evaluated after joins, so the conditional filtering you specify in WHERE will apply after the joins have taken place. So if you wanted all rows only where table2's rows didn't match table1's rows in a LEFT JOIN, you would specify WHERE table2.fieldname IS NULL.
It sounds like you want an inner join instead of an outer join. An inner join only returns those combinations where the join condition succeeds. A left join will always return at least one row for each row in the left table.
To get only those products with an alt price, do something like:
SELECT A.styleNo, A.price, B.price
FROM arts A
INNER JOIN pricelists B ON
B.seller = A.seller
AND B.ean = A.ean
AND B.alt_price is not null
WHERE...
Alternately, you can add AND B.alt_price is not null to the where clause of your current query, though that's likely to be less efficient unless your db's query optimizer takes over.
Ok. Finally found the solution.
The problem is, the search will contain multiple sellers, some of whome use pricelists and some who don't.
If I do an INNER JOIN on pricelists B, I will not get the products of sellers who don't use pricelists as they will not be have any entry in B.
If I do a LEFT JOIN on pricelists B, all entries will have either NULL or pricelist B values, so when I'm searching for a seller who uses pricelists for some of his products, I will always get his full range, regardless of alt_price specified
If I try to filter these unwanted records (seller uses pricelists, exclude products which are not on it), by adding B.alt_price != 0 to the WHERE clause, I'm also excluding all products from sellers not using pricelists.
I solved it like this:
- I'm having to construct this line anyway:
LEFT JOIN pricelists B ON
// dynamic construct depending on number of sellers using pricelists matched to user
B.seller = A.seller
AND B.ean = A.ean
AND B.alt_price != 0
So I created another variable, that includes all seller IDs who use pricelists and are applicable to this user. Looks like this:
123,456,789...
I add this to the WHERE clause:
AND ( IF( A.seller IN ( 123,456,789... ), B.alt_price IS NOT NULL,1 ) )
This way, I'm checking
(a) if the record is from a pricelist seller applicable to the user, and
(b) if that's the case, the records must not have a NULL value in the b.alt_price, which records not being on the pricelist will have, since sql adds NULL to all records not on the pricelist B when LEFT JOINING.
That was difficult...