Summing a column over a date range in a CTE? - sql

I'm trying to sum a certain column over a certain date range. The kicker is that I want this to be a CTE, because I'll have to use it multiple times as part of a larger query. Since it's a CTE, it has to have the date column as well as the sum and ID columns, meaning I have to group by date AND ID. That will cause my results to be grouped by ID and date, giving me not a single sum over the date range, but a bunch of sums, one for each day.
To make it simple, say we have:
create table orders (
id int primary key,
itemID int foreign key references items.id,
datePlaced datetime,
salesRep int foreign key references salesReps.id,
price int,
amountShipped int);
Now, we want to get the total money a given sales rep made during a fiscal year, broken down by item. That is, ignoring the fiscal year bit:
select itemName, sum(price) as totalSales, sum(totalShipped) as totalShipped
from orders
join items on items.id = orders.itemID
where orders.salesRep = '1234'
group by itemName
Simple enough. But when you add anything else, even the price, the query spits out way more rows than you wanted.
select itemName, price, sum(price) as totalSales, sum(totalShipped) as totalShipped
from orders
join items on items.id = orders.itemID
where orders.salesRep = '1234'
group by itemName, price
Now, each group is (name, price) instead of just (name). This is kind of sudocode, but in my database, just this change causes my result set to jump from 13 to 32 rows. Add to that the date range, and you really have a problem:
select itemName, price, sum(price) as totalSales, sum(totalShipped) as totalShipped
from orders
join items on items.id = orders.itemID
where orders.salesRep = '1234'
and orderDate between 150101 and 151231
group by itemName, price
This is identical to the last example. The trouble is making it a CTE:
with totals as (
select itemName, price, sum(price) as totalSales, sum(totalShipped) as totalShipped, orderDate as startDate, orderDate as endDate
from orders
join items on items.id = orders.itemID
where orders.salesRep = '1234'
and orderDate between startDate and endDate
group by itemName, price, startDate, endDate
)
select totals_2015.itemName as itemName_2015, totals_2015.price as price_2015, ...
totals_2016.itemName as itemName_2016, ...
from (
select * from totals
where startDate = 150101 and endDate = 151231
) totals_2015
join (
select *
from totals
where startDate = 160101 and endDate = 160412
) totals_2016
on totals_2015.itemName = totals_2016.itemName
Now the grouping in the CTE is way off, more than adding the price made it. I've thought about breaking the price query into its own subquery inside the CTE, but I can't escape needing to group by the dates in order to get the date range. Can anyone see a way around this? I hope I've made things clear enough. This is running against an IBM iSeries machine. Thank you!

Depending on what you are looking for, this might be a better approach:
select 'by sales rep' breakdown
, salesRep
, '' year
, sum(price * amountShipped) amount
from etc
group by salesRep
union
select 'by sales rep and year' breakdown
, salesRep
, convert(char(4),orderDate, 120) year
, sum(price * amountShipped) amount
from etc
group by salesRep, convert(char(4),orderDate, 120)
etc

When possible group by the id columns or foreign keys because the columns are indexed already you'll get faster results. This applies to any database.
with cte as (
select id,rep, sum(sales) sls, count(distinct itemid) did, count(*) cnt from sommewhere
where date between x and y
group by id,rep
) select * from cte order by rep
or more fancy
with cte as (
select id,rep, sum(sales) sls, count(distinct itemid) did, count(*) cnt from sommewhere
where date between x and y
group by id,rep
) select * from cte join reps on cte.rep = reps.rep order by sls desc

I eventually found a solution, and it doesn't need a CTE at all. I wanted the CTE to avoid code duplication, but this works almost as well. Here's a thread explaining summing conditionally that does exactly what I was looking for.

Related

Joining 2 columns of a subqueried table into SQL

I'm trying to match the inventory for the day to the SKU.
Amazon_orders has the SKUs I want to match
Invhistory2 has the SKU, timestamp date (more than 1, here's the issue), and the inventory quantity
I'm trying to create a subquery that averages the inventory for the day, then join the timestamp and SKU to the SKUs on the amazon orders table. Null values are no issue here.
My code looks like this:
(SELECT AVG(quantity) AS qty FROM `perfect-obelisk-289514.inventory_history.invhistory2`) AS qtyok
FROM `perfect-obelisk-289514.reports.flat_file_orders_by_order_datereport` Amazon_Orders
LEFT JOIN (SELECT AVG(quantity) AS qty, sku, CAST(snapshot_date AS DATE) AS invdate
FROM `perfect-obelisk-289514.inventory_history.invhistory2`
GROUP BY sku, invdate) AS table2
ON (
Amazon_Orders.sku = table2.sku AND CAST(LEFT(Amazon_orders.purchase_date,10) AS DATE) = table2.invdate
)
The issue is that I get the same average for each row, it's not joining the quantity using SKU and date.
As you may notice I'm a beginner, looked thoroughly but can't find the solution, any help is appreciated!
Thanks for the help!
I managed to solve it. My code was redundant, I just needed this join:
LEFT JOIN (SELECT AVG(quantity) AS qty,
sku,
CAST(snapshot_date AS DATE) AS invdate
FROM `perfect-obelisk-289514.inventory_history.invhistory2`
GROUP BY sku, invdate) AS table2
ON
(Amazon_Orders.sku = table2.sku AND CAST(LEFT(Amazon_orders.purchase_date,10) AS DATE) = table2.invdate)
This gives me what I need

Tweaking a Query - looking for duplicates within a certain day range

I posted a question similar to this, and got an answer, but the answer isn't configurable - my fault I should have been more clear, so I'll try again.
I have a table where TABLENAME has the following information - OrderDate, OrderNumber, CustomerID, ProductSKU, ProductName exist. This table has lines for invoices. So an order will have a data line for every item in the order.
I want to know, which customers have ordered the same item, more than once, where the order is within 90 of any other order of that same product by that customer, after a specific date. Same product in the same order number do not count. The catch is that I want "more than once" to be configurable, so if I need to see 3 or more, or 4 or more I can adjust AND I want to see the counts. Here's the query I have so far, which I think gives me the items and the counts - but not the 90 day thing:
EDITED: I don't think the former version gave me the right counts
SELECT customerid, productsku, productname, count(distinct ordernumber) FROM tablename
WHERE orderdate >'2017-11-01'
GROUP BY customerid, productsku, productname
HAVING COUNT(distinct ordernumber) > 2
Try doing this. it'll go back 90 days
declare #date date = '2017-11-01'
SELECT customerid, productsku, productname, count(distinct ordernumber) FROM tablename
WHERE orderdate >= dateadd(DD,-90,#date) and orderdate <= #date
GROUP BY customerid, productsku, productname
HAVING COUNT(distinct ordernumber) > 1
yes that is what I was doing in the first query. so this might be a really crappy way of doing it but without seeing any data it was kind of tough. this query shows gives you the order dates as well. hope it helps
WITH DupsWithin90Days (customerid,productsku,productname,orderdate,num)
as
(
select customerid,productsku,productname,orderdate ,count(*) num from (
SELECT X.customerid, X.productsku, X.productname,X.ORDERDATE,ROW_NUMBER() OVER (partition by x.customerid,x.orderdate order by x.orderdate) rownum
FROM
(
SELECT T1.customerid, T1.productsku, T1.productname,T1.ORDERDATE
FROM TABLENAME1 T1
) X
JOIN
(
SELECT T2.customerid, T2.productsku, T2.productname,T2.ORDERDATE
FROM
TABLENAME1 T2
) Y
ON X.customerid = Y.customerid AND X.orderdate >= dateadd(DD,-90,Y.orderdate)
) dup
where rownum > 1
group by customerid,productsku,productname,orderdate
)
select customerid,productsku,productname,orderdate
from DupsWithin90Days
order by customerid ,orderdate desc

I want to fetch this data year wise

Select
CIDetail.Itemname,
sum(CIDetail.TaxAmount+ CIDetail.LineAmount) As [TotalAmount]
From
CIDetail (Nolock)
INNER JOIN CIHeader On CIDetail.InvoiceNo= CIHeader.InvoiceNo
Where
CIHeader.InvoiceDate Between '2010-04-01' AND '2014-04-01'
Group By
CIDetail.Itemname
Have a derived table where you use ANSI SQL's EXTRACT to get the year part out of the date, and also add the amounts together. At main level you do GROUP BY both Itemname and year:
Select Itemname, "year", SUM(Amount) as TotalAmount
from
(
CIDetail.Itemname,
extract(year from CIHeader.InvoiceDate) as "year",
CIDetail.TaxAmount + CIDetail.LineAmount As Amount
From
CIDetail (Nolock)
INNER JOIN CIHeader On CIDetail.InvoiceNo= CIHeader.InvoiceNo
) dt
Group By
Itemname, "year"
No dbms tagged in question, but if your dbms doesn't support EXTRACT, try YEAR(CIHeader.InvoiceDate) for example, or something else.
Add to the query a order by clause. Here as you are saying that you want it by date, I will assume that one itemname has only one InvoiceDate. And thus its max will have the same value. You will have to do it as follows
Select CIDetail.Itemname,
sum(CIDetail.TaxAmount+ CIDetail.LineAmount) As [TotalAmount],
From CIDetail (Nolock) INNER JOIN CIHeader
On CIDetail.InvoiceNo= CIHeader.InvoiceNo
Where CIHeader.InvoiceDate Between '2010-04-01' AND '2014-04-01' Group By
CIDetail.Itemname
Order By max(CIHeader.InvoiceDate) ASC

How to do a group by without having to pass all the columns from the select?

I have the following select, whose goal is to select all customers who had no sales since the day X, and also bringing the date of the last sale and the number of the sale:
select s.customerId, s.saleId, max (s.date) from sales s
group by s.customerId, s.saleId
having max(s.date) <= '05-16-2013'
This way it brings me the following:
19 | 300 | 26/09/2005
19 | 356 | 29/09/2005
27 | 842 | 10/05/2012
In another words, the first 2 lines are from the same customer (id 19), I wish to get only one record for each client, which would be the record with the max date, in the case, the second record from this list.
By that logic, I should take off s.saleId from the "group by" clause, but if I do, of course, I get the error:
Invalid expression in the select list (not contained in either an
aggregate function or the GROUP BY clause)
I'm using Firebird 1.5
How can I do this?
GROUP BY summarizes data by aggregating a group of rows, returning one row per group. You're using the aggregate function max(), which will return the maximum value from one column for a group of rows.
Let's look at some data. I renamed the column you called "date".
create table sales (
customerId integer not null,
saleId integer not null,
saledate date not null
);
insert into sales values
(1, 10, '2013-05-13'),
(1, 11, '2013-05-14'),
(1, 12, '2013-05-14'),
(1, 13, '2013-05-17'),
(2, 20, '2013-05-11'),
(2, 21, '2013-05-16'),
(2, 31, '2013-05-17'),
(2, 32, '2013-03-01'),
(3, 33, '2013-05-14'),
(3, 35, '2013-05-14');
You said
In another words, the first 2 lines are from the same customer(id 19), i wish he'd get only one record for each client, which would be the record with the max date, in the case, the second record from this list.
select s.customerId, max (s.saledate)
from sales s
where s.saledate <= '2013-05-16'
group by s.customerId
order by customerId;
customerId max
--
1 2013-05-14
2 2013-05-16
3 2013-05-14
What does that table mean? It means that the latest date on or before May 16 on which customer "1" bought something was May 14; the latest date on or before May 16 on which customer "2" bought something was May 16. If you use this derived table in joins, it will return predictable results with consistent meaning.
Now let's look at a slightly different query. MySQL permits this syntax, and returns the result set below.
select s.customerId, s.saleId, max(s.saledate) max_sale
from sales s
where s.saledate <= '2013-05-16'
group by s.customerId
order by customerId;
customerId saleId max_sale
--
1 10 2013-05-14
2 20 2013-05-16
3 33 2013-05-14
The sale with ID "10" didn't happen on May 14; it happened on May 13. This query has produced a falsehood. Joining this derived table with the table of sales transactions will compound the error.
That's why Firebird correctly raises an error. The solution is to drop saleId from the SELECT clause.
Now, having said all that, you can find the customers who have had no sales since May 16 like this.
select distinct customerId from sales
where customerID not in
(select customerId
from sales
where saledate >= '2013-05-16')
And you can get the right customerId and the "right" saleId like this. (I say "right" saleId, because there could be more than one on the day in question. I just chose the max.)
select sales.customerId, sales.saledate, max(saleId)
from sales
inner join (select customerId, max(saledate) max_date
from sales
where saledate < '2013-05-16'
group by customerId) max_dates
on sales.customerId = max_dates.customerId
and sales.saledate = max_dates.max_date
inner join (select distinct customerId
from sales
where customerID not in
(select customerId
from sales
where saledate >= '2013-05-16')) no_sales
on sales.customerId = no_sales.customerId
group by sales.customerId, sales.saledate
Personally, I find common table expressions make it easier for me to read SQL statements like that without getting lost in the SELECTs.
with no_sales as (
select distinct customerId
from sales
where customerID not in
(select customerId
from sales
where saledate >= '2013-05-16')
),
max_dates as (
select customerId, max(saledate) max_date
from sales
where saledate < '2013-05-16'
group by customerId
)
select sales.customerId, sales.saledate, max(saleId)
from sales
inner join max_dates
on sales.customerId = max_dates.customerId
and sales.saledate = max_dates.max_date
inner join no_sales
on sales.customerId = no_sales.customerId
group by sales.customerId, sales.saledate
then you can use following query ..
EDIT changes made after comment by likeitlikeit for only one row per CustomerID even when we will have one case where we have multiple saleID for customer with certain condition -
select x.customerID, max(x.saleID), max(x.x_date) from (
select s.customerId, s.saleId, max (s.date) x_date from sales s
group by s.customerId, s.saleId
having max(s.date) <= '05-16-2013'
and max(s.date) = ( select max(s1.date)
from sales s1
where s1.customeId = s.customerId))x
group by x.customerID
You can Try Maxing the s.saleId (Max(s.saleId)) and removing it from the Group By clause
A subquery should do the job, I can't test it right now but it seems ok:
SELECT s.customerId, s.saleId, subq.maxdate
FROM sales AS s
INNER JOIN (SELECT customerId, MAX(date) AS maxdate
FROM sales
GROUP BY customerId, saleId
HAVING MAX(s.date) <= '05-16-2013'
) AS subq
ON s.customerId = subq.customerId AND s.date = subq.maxdate

In Oracle SQL, how do you query the proportion of records of a certain value?

Say, you have a query like
SELECT COUNT(*), date FROM ORDERS GROUP BY date ORDER BY date
but you also want to have a third "phantom/dummy field", where it basically tells you the fraction of orders each day that are of a particular type (lets say "Utensils" and "Perishables").
I should say that there is an additional column in the ORDERS table that has the type of the order:
order_type
The third dummy column should do something like take the count of orders on a date that have the "Utensils" or the "Perishables" type (not XOR), then divide by the total count of orders of that day, and then round to 2 decimal points, and append a percentage sign.
The last few formatting things, aren't really important...all I really need to know is how to apply the logic in valid PLSQL syntax.
Example output
4030 2012-02-02 34.43%
4953 2012-02-03 16.66%
You can do something like
SELECT COUNT(*),
dt,
round( SUM( CASE WHEN order_type = 'Utensils'
THEN 1
ELSE 0
END) * 100 / COUNT(*),2) fraction_of_utensils_orders
FROM ORDERS
GROUP BY dt
ORDER BY st
If you find it easier to follow, you could also
SELECT COUNT(*),
dt,
round( COUNT( CASE WHEN order_type = 'Utensils'
THEN 1
ELSE NULL
END) * 100/ COUNT(*), 2) fraction_of_utensils_orders
FROM ORDERS
GROUP BY dt
ORDER BY st
To Add sum of orders of same type to query:
select
o.*,
(
select count(o2.OrderType)
from ORDERS o2
where o2.OrderType = o.OrderType
) as NumberOfOrdersOfThisType
from ORDERS o
To Add fraction of orders of same type to query:
(Check variable definition to make sure it is PL/SQL)
declare totalCount number
select count(*)
into totalCount
from ORDERS
select
o.*,
(
select count(o2.OrderType)
from ORDERS o2
where o2.OrderType = o.OrderType
) / totalCount as FractionOfOrdersOfThisType
from ORDERS o