Translating subquery to left join in sqlite - sql

I have a query that is running against a SQLite database that uses a couple of subqueries. In order to accommodate some new requirements, I need to translate it to use joins instead. Below is the structure version of the original query:
SELECT c.id AS category_id, b.budget_year,
(
SELECT sum(actual)
FROM lines l1
WHERE status = 'complete'
AND category_id = c.id
AND billing_year = b.budget_year
) AS actual
(
SELECT sum(planned)
FROM lines l2
WHERE status IN ('forecasted', 'in-progress')
AND category_id = c.id
AND billing_year = b.budget_year
) AS rough_proposed
FROM categories AS c
LEFT OUTER JOIN budgets AS b ON (c.id = b.category_id)
GROUP BY c.id, b.budget_year;
The next query is my first attempt to convert it to use LEFT OUTER JOINs:
SELECT c.id AS category_id, b.budget_year, sum(l1.actual) AS actual, sum(l2.planned) AS planned
FROM categories AS c
LEFT OUTER JOIN budgets AS b ON (c.id = b.category_id)
LEFT OUTER JOIN lines AS l1 ON (l1.category_id = c.id
AND l1.billing_year = b.budget_year
AND l1.status = 'complete')
LEFT OUTER JOIN lines AS l2 ON (l2.category_id = c.id
AND l2.billing_year = b.budget_year
AND l2.status IN ('forecasted', 'in-progress'))
GROUP BY c.id, b.budget_year;
However, the actual and rough_proposed columns are much larger than expected. I am no SQL expert, and I am having a hard time understanding what is going on here. Is there a straightforward way to convert the subqueries to joins?

There is a problem with both your queries. However, the first query hides the problem, while the second query makes it visible.
Here is what's going on: you join lines twice - once as l1 and once more as l2. The query before grouping would have the same line multiple times when there are both actual lines and forecast-ed / in-progress lines. When this happens, each line would be counted multiple times, resulting in inflated values.
The first query hides this, because it does not apply aggregation to actual and rough_proposed columns. SQLite picks the first entry for each group, which has the correct value.
You can fix your query by joining to lines only once, and counting the amounts conditionally, like this:
SELECT
c.id AS category_id
, b.budget_year
, SUM(CASE WHEN l.status = 'complete' THEN l.actual END) AS actual
, SUM(CASE WHEN l.status IN ('forecasted', 'in-progress') THEN l.planned END) AS planned
FROM categories AS c
LEFT OUTER JOIN budgets AS b ON (c.id = b.category_id)
LEFT OUTER JOIN lines AS l ON (l.category_id = c.id AND l1.billing_year = b.budget_year)
GROUP BY c.id, b.budget_year
In this new query each row from lines is brought in only once; the decision to count it in one of the actual/planned columns is made inside the conditional expression embedded in the SUM aggregating function.

Related

sql multiple left joins with sum

I have 3 tables as below. What I need to do is create a sumamry after left joining the 1st table to the 2nd and the 2nd to the 3rd.
The code I'm using ends up resulting in a cartesian join. My query to create the 1st table (person) is complicated and resource intensive while the volume of data is table 2(shopping list) is massive so having a nested query is not ideal. Below is the code I'm using right now and the expected output (image 1) & what I get (image 2)
select
a.ID,
a.Name,
sum(b.cost) total_cost,
sum(c.discount_amount) total_discount
from
person a,
left join shopping_list b on a.id=b.id
left join discount c on b.item = c.item
group by
a.ID,
a.Name
I've looked at the below links but I was hoping there's a solution that may work better give the size of my dataset
https://dba.stackexchange.com/questions/217220/how-i-use-multiple-sum-with-multiple-left-joins
Multiple Left Join with sum
Thanks in advance for your help
You have multiple rows for the discounts, so presummarize those:
select p.id, p.name, coalesce(sl.cost, 0) as cost,
coalesce(d.discount_amount, 0) as discount_amount
from person p left join
shopping_list sl
on sl.id = p.id left join
(select d.item, sum(discount_amount) as discount_amount
from discount
group by d.item
) d
on sl.item = d.item
group by p.id, p.name;
The problem with your query is that the multiple rows of discount end up multiplying the rows of shopping_list -- resulting in the inaccurate totals.
Notice that in this query, the table aliases are abbreviations for the table names. This is a best practice that makes it much, much easier to follow the logic of a query.

SQL Get aggregate as 0 for non existing row using inner joins

I am using SQL Server to query these three tables that look like (there are some extra columns but not that relevant):
Customers -> Id, Name
Addresses -> Id, Street, StreetNo, CustomerId
Sales -> AddressId, Week, Total
And I would like to get the total sales per week and customer (showing at the same time the address details). I have come up with this query
SELECT a.Name, b.Street, b.StreetNo, c.Week, SUM (c.Total) as Total
FROM Customers a
INNER JOIN Addresses b ON a.Id = b.CustomerId
INNER JOIN Sales c ON b.Id = c.AddressId
GROUP BY a.Name, c.Week, b.Street, b.StreetNo
and even if my SQL skill are close to none it looks like it's doing its job. But now I would like to be able to show 0 whenever the one customer don't have sales for a particular week (weeks are just integers). And I wonder if somehow I should get distinct values of the weeks in the Sales table, and then loop through them (not sure how)
Any help?
Thanks
Use CROSS JOIN to generate the rows for all customers and weeks. Then use LEFT JOIN to bring in the data that is available:
SELECT c.Name, a.Street, a.StreetNo, w.Week,
COALESCE(SUM(s.Total), 0) as Total
FROM Customers c CROSS JOIN
(SELECT DISTINCT s.Week FROM sales s) w LEFT JOIN
Addresses a
ON c.CustomerId = a.CustomerId LEFT JOIN
Sales s
ON s.week = w.week AND s.AddressId = a.AddressId
GROUP BY c.Name, a.Street, a.StreetNo, w.Week;
Using table aliases is good, but the aliases should be abbreviations for the table names. So, a for Addresses not Customers.
You should generate a week numbers, rather than using DISTINCT. This is better in terms of performance and reliability. Then use a LEFT JOIN on the Sales table instead of an INNER JOIN:
SELECT a.Name
,b.Street
,b.StreetNo
,weeks.[Week]
,COALESCE(SUM(c.Total),0) as Total
FROM Customers a
INNER JOIN Addresses b ON a.Id = b.CustomerId
CROSS JOIN (
-- Generate a sequence of 52 integers (13 x 4)
SELECT ROW_NUMBER() OVER (ORDER BY a.x) AS [Week]
FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) a(x)
CROSS JOIN (SELECT x FROM (VALUES(1),(1),(1),(1)) b(x)) b
) weeks
LEFT JOIN Sales c ON b.Id = c.AddressId AND c.[Week] = weeek.[Week]
GROUP BY a.Name
,b.Street
,b.StreetNo
,weeks.[Week]
Please try the following...
SELECT Name,
Street,
StreetNo,
Week,
SUM( CASE
WHEN Total IS NULL THEN
0
ELSE
Total
END ) AS Total
FROM Customers a
JOIN Addresses b ON a.Id = b.CustomerId
RIGHT JOIN Sales c ON b.Id = c.AddressId
GROUP BY a.Name,
c.Week,
b.Street,
b.StreetNo;
I have modified your statement in three places. The first is I changed your join to Sales to a RIGHT JOIN. This will join as it would with an INNER JOIN, but it will also keep the records from the table on the right side of the JOIN that do not have a matching record or group of records on the left, placing NULL values in the resulting dataset's fields that would have come from the left of the JOIN. A LEFT JOIN works in the same way, but with any extra records in the table on the left being retained.
I have removed the word INNER from your surviving INNER JOIN. Where JOIN is not preceded by a join type, an INNER JOIN is performed. Both JOIN and INNER JOIN are considered correct, but the prevailing protocol seems to be to leave the INNER out, where the RDBMS allows it to be left out (which SQL-Server does). Which you go with is still entirely up to you - I have left it out here for illustrative purposes.
The third change is that I have added a CASE statement that tests to see if the Total field contains a NULL value, which it will if there were no sales for that Customer for that Week. If it does then SUM() would return a NULL, so the CASE statement returns a 0 instead. If Total does not contain a NULL value, then the SUM() of all values of Total for that grouping is performed.
Please note that I am assuming that Total will not have any NULL values other than from the RIGHT JOIN. Please advise me if this assumption is incorrect.
Please also note that I have assumed that either there will be no missing Weeks for a Customer in the Sales table or that you are not interested in listing them if there are. Again, please advise me if this assumption is incorrect.
If you have any questions or comments, then please feel free to post a Comment accordingly.

Joining 3 tables on 2 columns?

I've created 3 views with identical columns- Quantity, Year, and Variety. I want to join all three tables on year and variety in order to do some calculations with quantities.
The problem is that a particular year/variety combo does not occur on every view.
I've tried queries like :
SELECT
*
FROM
a
left outer join
b
on a.variety = b.variety
left outer join
c
on a.variety = c.variety or b.variety = c.variety
WHERE
a.year = '2015'
and b.year = '2015'
and a.year= '2015'
Obviously this isn't the right solution. Ideally I'd like to join on both year and variety and not use a where statement at all.
The desired output would be put all quantities of matching year and variety on the same line, regardless of null values on a table.
I really appreciate the help, thanks.
You want a full outer join, not a left join, like so:
Select coalesce(a.year, b.year, c.year) as Year
, coalesce(a.variety, b.variety, c.variety) as Variety
, a.Quantity, b.Quantity, c.Quantity
from tableA a
full outer join tableB b
on a.variety = b.variety
and a.year = b.year
full outer join tableC c
on isnull(a.variety, b.variety) = c.variety
and isnull(a.year, b.year) = c.year
where coalesce(a.year, b.year, c.year) = 2015
The left join you are using won't pick up values from b or c that aren't in a. Additionally, your where clause is dropping rows that don't have values in all three tables (because the year in those rows is null, which is not equal to 2015). The full outer join will grab rows from either table in the join, regardless of whether the other table contains a match.

Translate an SQL subquery to a more efficient version

I'm working with a moderately large MSAccess .mdb file that I need to manipulate with SQL. Unfortunately some statements which work in theory seem to cause it to hang, and I've run into a brick wall.
Here is a simplified representation in SQL Fiddle
Three tables: products, product_category, and categories
I need to SELECT categories that ONLY contain items that have the field 'HIDE = 1'
If a category contains products that are hide = 0, it should not be selected.
I can do this relatively easily with subqueries, but the query stalls out. In the past queries that rely on left joins seem to execute efficiently, but I cannot wrap my mind around joins enough to translate this query into that format.
EDIT:
SELECT c.categoryid
FROM product_category AS c
LEFT JOIN
(
SELECT DISTINCT c.categoryid
FROM product_category AS c
LEFT JOIN products AS p
ON c.catalogid = p.catalogid
WHERE p.hide = 0
) y ON y.categoryid = c.categoryid
WHERE y.categoryid IS NULL
Someone posted the above query as an answer but then for some reason deleted it. As far as I can tell it works and works quickly. I consider this question to be answered. If I remember I will self-post the answer once the timer allows me to.
I believe you just need to un-correlate the subquery eg...
SELECT c.categoryid FROM product_category AS c
WHERE c.categoryid NOT IN
(SELECT DISTINCT c1.categoryid FROM product_category AS c1
LEFT JOIN products AS p ON c1.catalogid = p.catalogid
WHERE p.hide = 0)
Note how I have aliased the subquery product_category table as c1 instead of c - This means the subquery will only execute once as opposed to once for every row of the your main query.
SQL Fiddle
Note that there will no doubt be more efficiencies still to be found however I think this will suffice for your purposes.
In fact there is no need for a LEFT JOIN here I don't think ie...
SELECT c.categoryid FROM product_category AS c
WHERE c.categoryid NOT IN
(SELECT DISTINCT c1.categoryid FROM product_category AS c1
INNER JOIN products AS p ON c1.catalogid = p.catalogid
WHERE p.hide = 0)
..This will afford you some extra speed.
If there is only one categoryid per catalogid then you can get rid of the distinct:
Select
c.id, c.categoryname
From
category c
Where
Not Exists (
Select
'x'
From
products p
Inner Join
product_category pc
on pc.catalogid = p.catalogid
Where
pc.categoryid = c.id and
p.hide = 0
)
Edited - the test data in the fiddle seems wrong, I've corrected it. This should work now
http://sqlfiddle.com/#!6/56f5e/1/0

Left outer join and group by issue

I wrote a query. this query sum fields from 2 different table. And grouped by main table id field. But second left outer join is not grouped and giving me different results.
SELECT s.*,
f.firma_adi,
sum(sd.fiyat) AS konak,
sum(ss.fiyat) AS sponsor
FROM fuar_sozlesme1 s
INNER JOIN fuar_firma_2012 f
ON ( s.cari = f.cari )
LEFT OUTER JOIN fuar_sozlesme1_detay sd
ON ( sd.sozlesme_id = s.id )
LEFT OUTER JOIN fuar_sozlesme1_sponsor ss
ON ( ss.sozlesme_id = s.id )
GROUP BY s.id
ORDER BY s.id DESC
I know, it is really complicated but I'm stucking on this issue.
My question is: why second left outer join is not correctly sum of field . If I remove second left outer join or first, everything is normal.
The problem is that you have multiple dimensions on your data, and the number of rows is multiplying beyond what you expect. I would suggest that you run the query for one id, without the group by, to see what rows the join is producing.
One way to fix this is by using correlated subqueries:
select s.*, f.firma_adi,
(select SUM(sd.fiyat)
from fuar_sozlesme1_detay fd
where sd.sozlesme_id = s.id
) as konak,
(select SUM(ss.fiyat)
from fuar_sozlesme1_sponsor ss
where (ss.sozlesme_id = s.id)
) as sponsor
from fuar_sozlesme1 s inner join
fuar_firma_2012 f
on (s.cari = f.cari)
order by s.id DESC
By the way, you appear to by using MySQL (because your query is not parsable in any other dialect). You should tag your questions with the version of the database you are using.