get continuosly increasing sale records from table sql server - sql

I have 2 tables
Product
ProdId, ProdName
1 A
2 B
and
Sale
SaleId, ProdId, Sale, Year
1, 1, 100, 2012
2, 1, 130, 2013
3, 2, 100, 2012,
4, 1, 150, 2014,
5, 1, 180, 2015
6, 2, 120, 2013,
7, 2, 90, 2014,
8, 2, 130, 2015
I want the name of product whose sale is continuosly increasing.
Like Product "A" has sale record like in year 2012 - 100 Units,2013 - 130 Units,2014 - 150 Units,2015 - 180 Units, So this product A is having continuous increase in sale. Another case of non-continuous record is, product "B" having sale record 2012 - 100 Units,2013 - 120 Units,2014 - 90 Units, 2015 - 130 Units, So for product "B", it is not continuous.
I want records like product "A", who is having continuous increasing sale.
Help appreciated.

You can do this using row_number() twice:
select prod_id
from (select s.*,
row_number() over (partition by s.prod_id order by sale) as seqnum_s,
row_number() over (partition by s.prod_id order by year) as seqnum_y
from sales s
) s
group by prod_id
having sum( case when seqnum_s = seqnum_y then 1 else 0 end) = count(*);
That is, order by the year and the sales. When all row numbers are the same, then the sales are increasing.
Note: There are some cases where tied sales might be considered increasing. This can be handled by the logic -- either by excluding or including such situations. I have not included logic for this, because your question is not clear what to do in that situation.

Use cross apply to get the previous year's sale amount and check with conditional aggregation for the increasing amount condition.
select prodid
from sale s1
cross apply (select sale as prev_sale
from sale s2
where s1.prodid=s2.prodid and s2.year=s1.year-1) s2
group by prodid
having sum(case when sale-prev_sale<0 then 1 else 0 end) = 0
To get the all the rows for such prodId's, use
select * from sale
where prodid in (select prodid
from sale s1
cross apply (select sale as prev_sale
from sale s2
where s1.prodid=s2.prodid and s2.year=s1.year-1) s2
group by prodid
having sum(case when sale-prev_sale<0 then 1 else 0 end) = 0
)

Here's a way with a CTE
declare #sale table (SaleID int, ProdId int, Sale int, Year int)
insert into #sale
values
(1,1,100,2012),
(2,1,130,2013),
(3,2,100,2012),
(4,1,150,2014),
(5,1,180,2015),
(6,2,120,2013),
(7,2,90,2014),
(8,2,130,2015)
declare #product table (ProdID int, ProdName char(1))
insert into #product
values
(1,'A'),
(2,'B')
;with cte as(
select
row_number() over (partition by ProdId order by Year) as RN
,*
from #sale)
select
p.ProdName
,cte.*
from cte
inner join
#product p on
p.ProdID=cte.ProdId
where cte.ProdId IN
(select distinct
c1.ProdId
from cte c1
left join
cte c2 on c2.RN = c1.rn+1 and c2.ProdId = c1.ProdId
group by c1.ProdId
having min(case when c1.Sale < isnull(c2.Sale,999999) then 1 else 0 end) = 1)
RETURNS
+----------+----+--------+--------+------+------+
| ProdName | RN | SaleID | ProdId | Sale | Year |
+----------+----+--------+--------+------+------+
| A | 1 | 1 | 1 | 100 | 2012 |
| A | 2 | 2 | 1 | 130 | 2013 |
| A | 3 | 4 | 1 | 150 | 2014 |
| A | 4 | 5 | 1 | 180 | 2015 |
+----------+----+--------+--------+------+------+

Related

Joining invoices and shipping records based on partial info

I hope this is just a case of me not knowing the terminology to search for, but I haven't found any hint of how to solve this yet.
I am trying to join two tables (invoices and shipping records) where some of the info is missing. In particular the account code and order number which I would usually use to join on.
Given that each order is fairly unique in the exact mix of products and quantities I am hoping it is possible to join the tables by comparing the composition of the orders.
For example given the data below it ought to be possible to identify that the shipping record for order_ref A1 is related to invoice_num 500 as it contains the same products in exactly the same quantities.
shipping_id | order_ref | product | quantity
-------------|-----------|---------|----------
100 | A1 | Apple | 1
101 | A1 | Banana | 1
102 | A1 | Carrot | 2
invoice_num | line_num | product | quantity
-------------|----------|---------|----------
500 | 1 | Apple | 1
500 | 2 | Banana | 1
500 | 3 | Carrot | 2
501 | 1 | Apple | 10
501 | 2 | Banana | 1
501 | 3 | Carrot | 2
You can create a key for each group, and join with this key.
In your sample, Apple_1_Banana_1_Carrot_2_ key will create for order_ref = "A1" of shipping and invoice_num = "500" of invoice.
DECLARE #shipping TABLE (shipping_id INT, order_ref VARCHAR(10), product VARCHAR(10), quantity INT)
INSERT INTO #shipping VALUES
(100 , 'A1', 'Apple', 1),
(101 , 'A1', 'Banana', 1),
(102 , 'A1', 'Carrot', 2)
DECLARE #invoice TABLE (invoice_num INT, line_num INT, product VARCHAR(10), quantity INT)
INSERT INTO #invoice VALUES
(500, 1 ,'Apple', 1 ),
(500, 2 ,'Banana', 1 ),
(500, 3 ,'Carrot', 2 ),
(501, 1 ,'Apple', 10 ),
(501, 2 ,'Banana', 1 ),
(501, 3 ,'Carrot', 2 )
SELECT * FROM (
SELECT * FROM #shipping s
CROSS APPLY(SELECT product + '_' + CAST(quantity AS varchar(10)) + '_'
FROM #shipping s2 WHERE s.order_ref = s2.order_ref
ORDER BY product , quantity FOR XML PATH('')) X(group_key)
) A
INNER JOIN
(SELECT * FROM #invoice i
CROSS APPLY(SELECT product + '_' + CAST(quantity AS varchar(10)) + '_'
FROM #invoice i2 WHERE i.invoice_num = i2.invoice_num
ORDER BY product , quantity FOR XML PATH('')) X(group_key)
)B ON A.group_key = B.group_key
AND A.product = B.product
AND A.quantity = B.quantity
Result:
shipping_id order_ref product quantity line_num invoice_num line_num product quantity
----------- ---------- ---------- ----------- -------------------- ----------- ----------- ---------- -----------
100 A1 Apple 1 1 500 1 Apple 1
101 A1 Banana 1 2 500 2 Banana 1
102 A1 Carrot 2 3 500 3 Carrot 2
join on product and quantity instead:
select table_a.*, table_b.*
from table_a
join table_b on table_a.product = table_b.product
and table_a.quantity = table_b.quantity
I don't think there is a proper SQL way to join like this but you could do something like the following:
SELECT order_ref, STRING_AGG(product + quantity, '_') as product_list
FROM
(SELECT * FROM shipping_records ORDER BY product) AS inner_shipping_records
GROUP BY
order_ref
and then
SELECT invoice_num, STRING_AGG(product + quantity, '_') as product_list
FROM
(SELECT * FROM invoices ORDER BY product) AS inner_invoices
GROUP BY
invoice_num
and then do your join on the product_list fields:
SELECT * FROM
( SELECT order_ref, STRING_AGG(.... ) as a_products JOIN
( SELECT invoice_num, STRING_AGG(.... ) as a_shipping_records
ON a_products.product_list = a_shipping_records.product_list
I haven't tested this on SQL Server but it should work. I don't think it would be fast but you could work out some kind of functional index or views that could speed this up.

Search for customers with 3 first payments, and have a specific combination

What I need is: I have to list all customer that have a payment plan (we charge every month of them) and have this combination: its first payment is done and the next 2 are still unpaid. The finance table have: ID, PAYMENTPLANID, DATEDUE, AMOUNTEDUE, AMOUNTPAID
I know that row is paid when AMOUNTPAID > 0 and the DATEDUE indicates
I need because I will cancel the payment plan in the end of the 3rd month if the customer only pays the first charge and stops for the next 2 month.
Is there a simpler way to identify this person that not involves 3 subselects (the only way I thought)?
Example:
+--------------------------------------------------------+
| ID, PAYMENTPLANID, DATEDUE, AMOUNTEDUE, AMOUNTPAID |
+--------------------------------------------------------+
| 1, 1, 2017-07-05, 10, 0 |
| 1, 1, 2017-06-05, 10, 0 |
| 1, 1, 2017-05-05, 10, 10 |
| 2, 5, 2017-07-05, 25, 25 |
| 2, 5, 2017-06-05, 25, 0 |
| 2, 5, 2017-05-05, 25, 25 |
+--------------------------------------------------------+
The payment plan with ID 1 should be canceled this month since the person only paid the first payment.
The payment plan 5 should not be cancelled.
You can use lead with the optional 2nd argument to look 1 and 2 rows ahead and check for your conditions.
select * --distinct paymentplanid /*if only planid is needed as output*/
from (
select t.*
,lead(amountpaid,1) over(partition by paymentplanid order by datedue) as nxt_1
,lead(amountpaid,2) over(partition by paymentplanid order by datedue) as nxt_2
,lag(amountpaid) over(partition by paymentplanid order by datedue) as prev_1
from tbl t
) t
where amountpaid>0 and prev_1 is null and nxt_1=0 and nxt_2=0
something like this should work ?
select ID , PaymentplandID from
(select ID , PaymentplandID , max(amount_paid ) over ( order by ID ,
PaymentplandID rows between current row and 1 following partion by Datedue desc )
as amount ,
row_number() over ( order by select ID , PaymentplandID partion by Datedue asc) as rn
from you_tble) t1
where t1.amount = 0 and rn = 3

SQL Server map payments to products

We have shopping carts that might have included products and payments.
Since these payments will be made to the carts, there will be no relation between the products and the payments except that they are in the same cart.
There are cases that these products will be invoiced individually even though they are in the same cart.
To create the invoices for the products we need the payment details, so we have to map the products to the payments.
These are our tables:
create table Products
(
ItemId int primary key,
CartId int not null,
ItemAmount smallmoney not null
)
create table Payments
(
PaymentId int primary key,
CartId int not null,
PaymentAmount smallmoney not null
)
create table MappedTable
(
ItemId int not null,
PaymentId int not null,
Amount smallmoney not null
)
INSERT INTO Products (ItemId, CartId, ItemAmount)
VALUES (1, 1, 143.49), (2, 1, 143.49), (3, 1, 143.49), (4, 2, 50.00), (5, 3, 75.00), (6, 3, 75.00)
INSERT INTO Payments (PaymentId, CartId, PaymentAmount)
VALUES (1, 1, 376.47), (2, 1, 54.00), (3, 2, 60.00), (4, 3, 140.00)
--select * from Products
--select * from Payments
--DROP TABLE Products
--DROP TABLE Payments
--DROP TABLE MappedTable
Products
ItemId | CartId | ItemAmount
------ | ------ | ----------
1 | 1 | 143.49
2 | 1 | 143.49
3 | 1 | 143.49
4 | 2 | 50.00
5 | 3 | 75.00
6 | 3 | 75.00
Payments
PaymentId | CartId | PaymentAmount
--------- | ------ | -------------
1 | 1 | 376.47
2 | 1 | 54.00
3 | 2 | 60.00
4 | 3 | 140.00
The order of the products and the payments may differ.
We need the output to look like this:
MappingTable
ItemId | PaymentId | MappedAmount
------ | --------- | ------------
1 | 1 | 143.49
2 | 1 | 143.49
3 | 1 | 89.49
3 | 2 | 54.00
4 | 3 | 50.00 (Remaining 10.00 from Payment 3 will be ignored)
5 | 4 | 75.00
6 | 4 | 65.00 (Missing 10.00 from Payment 4 will be ignored)
Cart 1: Sum of payments = sum of product costs
Cart 2: Sum of payments > sum of product costs. Only take the total product cost. Ignore the remaining 10.00
Cart 3: Sum of payments < sum of product costs. Take all the payments, ignore the fact that the payment is 10.00 short.
I thought that a query like the one below may solve the problem, but no luck.
insert into MappedTable
select
prd.ItemId, pay.PaymentId,
(Case
when prd.ItemAmount - isnull((select sum(m.Amount)
from MappedTable m
where m.ItemId = prd.ItemId), 0) <= pay.PaymentAmount - isnull((select sum(m.Amount) from MappedTable m where m.PaymentId = pay.PaymentId), 0)
then prd.ItemAmount - isnull((select sum(m.Amount) from MappedTable m where m.ItemId = prd.ItemId), 0)
else pay.PaymentAmount - isnull((select sum(m.Amount) from MappedTable m where m.PaymentId = pay.PaymentId), 0)
end)
from
Products prd
inner join
Payments pay on pay.CartId = prd.CartId
where
prd.ItemAmount > isnull((select sum(m.Amount) from MappedTable m where m.ItemId = prd.ItemId), 0)
and pay.PaymentAmount > isnull((select sum(m.Amount) from MappedTable m where m.PaymentId = pay.PaymentId), 0)
I've read about CTE (Common Table Expressions) and set-based approaches but I couldn't handle the issue.
So is this possible without using a cursor or a while loop?
Generally, this kind of task is referred to as a "knapsack problem", which is known to have no solution more efficient than brute forcing all possible combinations. In your case, however, you have additional conditions, namely ordered sets of both items and payments, so it is actually solvable using "overlapping intervals" technique.
The idea is to generate ranges of items and payments (1 pair of ranges per cart) and then look which payments overlap with which items, sequentially.
For any item-payment combination, there are 3 possible scenarios:
Payment covers the beginning of the item range (possibly completely covering the item);
Payment is completely inside the item;
Payment covers the end of item, thus "closing" it.
So, all that is needed is, for every item, find all suitable payments that match the aforementioned criteria, and order them by their identifiers. Here is a query that does it:
with cte as (
-- Project payment ranges, per cart
select pm.*, sum(pm.PaymentAmount) over(partition by pm.CartId order by pm.PaymentId) as [RT]
from #Payments pm
)
select q.ItemId, q.PaymentId,
-- Calculating the amount from payment that goes for this item
case q.Match
when 1 then q.PaymentRT - (q.ItemRT - q.ItemAmount)
when 2 then q.PaymentAmount
when 3 then case
-- Single payment spans over several items
when q.PaymentRT >= q.ItemRT and q.PaymentRT - q.PaymentAmount <= q.ItemRT - q.ItemAmount then q.ItemAmount
-- Payment is smaller than item
else q.ItemRT - (q.PaymentRT - q.PaymentAmount)
end
end as [Amount]
--, q.*
from (
select
sq.ItemId, pm.PaymentId, sq.ItemAmount, sq.RT as [ItemRT],
pm.PaymentAmount, pm.RT as [PaymentRT],
row_number() over(partition by sq.CartId, sq.ItemId, pm.PaymentId order by pm.RT) as [RN],
pm.Match
--, sq.CartId
from (
select pr.*, sum(pr.ItemAmount) over(partition by pr.CartId order by pr.ItemId) as [RT]
from #Products pr
) sq
outer apply (
-- First payment to partially cover this item
select top (1) c.*, 1 as [Match] from cte c where c.CartId = sq.CartId
and c.RT > sq.RT - sq.ItemAmount and c.RT < sq.RT
order by sq.RT
union all
-- Any payments that cover this item only
select c.*, 2 as [Match] from cte c where c.CartId = sq.CartId
and c.RT - c.PaymentAmount > sq.RT - sq.ItemAmount
and c.RT < sq.RT
union all
-- Last payment that covers this item
select top (1) c.*, 3 as [Match] from cte c where c.CartId = sq.CartId
and c.RT >= sq.RT
order by sq.RT
) pm
) q
where q.RN = 1;
The outer apply section is where I get payments related to each item. The only problem is, if a payment covers an item in its entirety, it will be listed several times. In order to remove these duplicates, I have ordered matches using row_number() and added an additional wrapping level - subquery with the q alias - where I cut off any duplicated rows by filtering the row number value.
P.S. If your SQL Server version is prior to 2012, you will need to calculate running totals using one of many approaches available on the Internet, because sum() over(order by ...) is only available on 2012 and later versions.

multiple transactions within a certain time period, limited by date range

I have a database of transactions, people, transaction dates, items, etc.
Each time a person buys an item, the transaction is stored in the table like so:
personNumber, TransactionNumber, TransactionDate, ItemNumber
What I want to do is to find people (personNumber) who, from January 1st 2012(transactionDate) until March 1st 2012 have purchased the same ItemNumber multiple times within 14 days (configurable) or less. I then need to list all those transactions on a report.
Sample data:
personNumber, TransactionNumber, TransactionDate, ItemNumber
1 | 100| 2001-01-31| 200
2 | 101| 2001-02-01| 206
2 | 102| 2001-02-11| 300
1 | 103| 2001-02-09| 200
3 | 104| 2001-01-01| 001
1 | 105| 2001-02-10| 200
3 | 106| 2001-01-03| 001
1 | 107| 2001-02-28| 200
Results:
personNumber, TransactionNumber, TransactionDate, ItemNumber
1 | 100| 2001-01-31| 200
1 | 103| 2001-02-09| 200
1 | 105| 2001-02-10| 200
3 | 104| 2001-01-01| 001
3 | 106| 2001-01-03| 001
How would you go about doing that?
I've tried doing it like so:
select *
from (
select personNumber, transactionNumber, transactionDate, itemNumber,
count(*) over (
partition by personNumber, itemNumber) as boughtSame)
from transactions
where transactionDate between '2001-01-01' and '2001-03-01')t
where boughtSame > 1
and it gets me this:
personNumber, TransactionNumber, TransactionDate, ItemNumber
1 | 100| 2001-01-31| 200
1 | 103| 2001-02-09| 200
1 | 105| 2001-02-10| 200
1 | 107| 2001-02-28| 200
3 | 104| 2001-01-01| 001
3 | 106| 2001-01-03| 001
The issue is that I don't want TransactionNumber 107, since that's not within the 14 days. I'm not sure where to put in that limit of 14 days. I could do a datediff, but where, and over what?
Alas, the window functions in SQL Server 2005 just are not quite powerful enough. I would solve this using a correlated subquery.
The correlated subquery counts the number of times that a person purchased the item within 14 days after each purchase (and not counting the first purchase).
select t.*
from (select t.*,
(select count(*)
from t t2
where t2.personnumber = t.personnumber and
t2.itemnumber = t.itemnumber and
t2.transactionnumber <> t.transactionnumber and
t2.transactiondate >= t.transactiondate and
t2.transactiondate < DATEADD(day, 14, t.transactiondate
) NumWithin14Days
from transactions t
where transactionDate between '2001-01-01' and '2001-03-01'
) t
where NumWithin14Days > 0
You may want to put the time limit in the subquery as well.
An index on transactions(personnumber, itemnumber, transactionnumber, itemdate) might help this run much faster.
If as your question states you just want to find people (personNumbers) with the specified criteria, you can do a self join and group by:
create table #tx (personNumber int, transactionNumber int, transactionDate dateTime, itemNumber int)
insert into #tx
values
(1, 100, '2001-01-31', 200),
(2, 101, '2001-02-01', 206),
(2, 102, '2001-02-11', 300),
(1, 103, '2001-02-09', 200),
(3, 104, '2001-01-01', 001),
(1, 105, '2001-02-10', 200),
(3, 106, '2001-01-03', 001),
(1, 107, '2001-02-28', 200)
declare #days int = 14
select t1.personNumber from #tx t1 inner join #tx t2 on
t1.personNumber = t2.personNumber
and t1.itemNumber = t2.itemNumber
and t1.transactionNumber < t2.transactionNumber
and datediff(day, t1.transactionDate, t2.transactionDate) between 0 and #days
group by t1.personNumber
-- if more than zero joined rows there is more than one transaction in period
having count(t1.personNumber) > 0
drop table #tx

Sql: Calc average times a customers ordered a product in a period

How would you calc how many times a product is sold in average in a week or month, year.
I'm not interested in the Amount, but how many times a customer has bought a given product.
OrderLine
OrderNo | ProductNo | Amount |
----------------------------------------
1 | 1 | 10 |
1 | 4 | 2 |
2 | 1 | 2 |
3 | 1 | 4 |
Order
OrderNo | OrderDate
----------------------------------------
1 | 2012-02-21
2 | 2012-02-22
3 | 2012-02-25
This is the output I'm looking for
ProductNo | Average Orders a Week | Average Orders a month |
------------------------------------------------------------
1 | 3 | 12 |
2 | 5 | 20 |
You would have to first pre-query it grouped and counted per averaging method you wanted. To distinguish between year 1 and 2, I would add year() of the transaction into the grouping qualifier for distinctness. Such as Sales in Jan 2010 vs Sales in 2011 vs 2012... similarly, week 1 of 2010, week 1 of 2011 and 2012 instead of counting as all 3 years as a single week.
The following could be done if you are using MySQL
select
PreCount.ProductNo,
PreCount.TotalCount / PreCount.CountOfYrWeeks as AvgPerWeek,
PreCount.TotalCount / PreCount.CountOfYrMonths as AvgPerMonth,
PreCount.TotalCount / PreCount.CountOfYears as AvgPerYear
from
( select
OL.ProductNo,
count(*) TotalCount,
count( distinct YEARWEEK( O.OrderDate ) ) as CountOfYrWeeks,
count( distinct Date_Format( O.OrderDate, "%Y%M" )) as CountOfYrMonths,
count( distinct Year( O.OrderDate )) as CountOfYears
from
OrderLine OL
JOIN Order O
on OL.OrderNo = O.OrderNo
group by
OL.ProductNo ) PreCount
This is a copy of DRapp's answer, but coded for SQL Server (it's too big for a comment!)
SELECT PreCount.ProductNo,
PreCount.TotalCount / PreCount.CountOfYrWeeks AS AvgPerWeek,
PreCount.TotalCount / PreCount.CountOfYrMonths AS AvgPerMonth,
PreCount.TotalCount / PreCount.CountOfYears AS AvgPerYear
FROM (SELECT OL.ProductNo,
Count(*) TotalCount,
Count(DISTINCT Datepart(wk, O.OrderDate)) AS CountOfYrWeeks,
Count(DISTINCT Datepart(mm, O.OrderDate)) AS CountOfYrMonths,
Count(DISTINCT Year(O.OrderDate)) AS CountOfYears
FROM OrderLine OL JOIN [Order] O
ON OL.OrderNo = O.OrderNo
GROUP BY OL.ProductNo) PreCount