Select rows so total number not to exceed a certain number - sql

I am using SQL Server 2008 and I have a SALES_ORDER table.
Each row of the table is one material. Many rows (materials) can belong to the same ORDER. I want to select the materials of many orders under two conditions:
All materials under the same order will be selected (orders will not be split).
The total number of selected rows will not exceed a maximum predefined number ROW_MAX.
For example, let's say we have
4 materials under order_1
3 materials under order_2
5 materials under order_3
2 materials under order_4
and ROW_MAX = 7
the query must return
all materials of the orders order_1 and order_2 (total 7 materials)
OR
all materials of orders order_1 and order_4 (total 6 materials)
OR
any other combination of orders so the total number or selected rows does not exceed 7.
Any idea how to do it with SQL script?

I believe it's a bit more tricky than that. I guess the following code answers the requisite:
any other combination of orders so the total number or selected rows does not exceed 7
There's little work to do before you can cross apply.
First we create a data sample in which order1 + order2 is bigger than 7 (so that we can test and doesn't stops looking after order2):
-- A sample table in which order1 + order 2 count is bigger than 7
IF OBJECT_ID('tempdb..#sample') is not null
DROP TABLE #sample
CREATE TABLE #sample (
OrderID INT,
OrderDesc VARCHAR(50)
)
INSERT INTO #sample VALUES
(1, 'Order 1 customer 1'),
(1, 'Order 2 customer 1'),
(1, 'Order 3 customer 1'),
(1, 'Order 4 customer 1'),
(2, 'Order 1 customer 2'),
(2, 'Order 2 customer 2'),
(2, 'Order 3 customer 2'),
(2, 'Order 4 customer 2'),
(2, 'Order 5 customer 2'),
(3, 'Order 1 customer 3'),
(3, 'Order 2 customer 3'),
(4, 'Order 1 customer 4')
Then we count the number of row for each order:
-- Counting rows for each order
IF OBJECT_ID('tempdb..#samplecount') is not null
DROP TABLE #samplecount
SELECT OrderID, COUNT(*) as OrderCount
INTO #samplecount
FROM #sample
GROUP BY OrderID
We order the orders according that count:
--Numbering each row
IF OBJECT_ID('tempdb..#samplecountordered') is not null
DROP TABLE #samplecountordered
SELECT *, ROW_NUMBER() OVER(ORDER BY OrderCount) AS OrderNumber
INTO #samplecountordered
from #samplecount
After that we can use cross apply on that organised repository:
select sa.*
from #samplecountordered so cross apply
(select SUM(OrderCount) as running_count
from #samplecountordered so2
where so2.OrderNumber <= so.OrderNumber
) so2
INNER JOIN #sample sa
ON sa.OrderID = so.orderID
where running_count <= 7;
This solution requires further testing, but I guess it is the spirit.
I use temporary tables so that it is easier to follow in terms of construction.
Sorry, I used customers instead of material.

You are looking for a cumulative sum. In SQL Server 2008, you can do this with apply:
select so.*
from sales_order so cross apply
(select count(*) as running_count
from sales_order so2
where so2.order_id <= so.order_id
) so2
where running_count <= 7;

Related

Count multiple columns in a query for multiple criteria

I have a query that should count the number of items used by department.
The first two tables give me the units and the persons who used the items.
The third table tells who used what.
STAFF(EMPID,EMPNAME,UNITCTR)
CAFUNIT(UNITCTR, UNITDSC)
CAFTRXHD(BILLNO,TRXDATE,ITEMCODE,ITEMPRICE,ITEMDESC,ITEMPRICE,EMPID)
This is the query
SELECT a.UNITCTR, b.UNITDSC, COUNT(c.ITEMCODE)
FROM UNIT.STAFF a, UNIT.CAFUNIT b, UNIT.CAFTRXHID c
WHERE a.UNITCTR = b.UNITCTR
AND c.ITEMCODE IN ('397', '398', '399', '400', '401', '402', '403')
GROUP BY a.UNITCTR, b.UNITDSC
This returns the count of all used items by department for example Department A used 200 of these items, so I get the Department ID, name and the total count of items.
123|Cafeteria|200
where 200 is the sum of all of these items (397 to 403)
I need to know the count for each item by department for instance Department A used 10 boxes of item 397 and 5 of item B and …
123|Cafeteria|20|20|50|30|50|10|70
Using what is suggested here isn't working or I am not doing it right. Any ideas?
Hello after checking you request, follows a query what I think could help you, I'm using two queries with partition, one to group all items by unit and another to group items by unit and item code.
WITH STAFF AS (
SELECT * FROM (
VALUES
(1, 'John Smith','U1'),
(2, 'David Thompson','U2'),
(3, 'Stacey Leigh','U3')
) AS _ (EMPID,EMPNAME,UNITCTR)
),CAFUNIT AS (
SELECT * FROM (
VALUES
('U1', 'Unit 1'),
('U2', 'Unit 2'),
('U3', 'Unit 3')
) AS _ (UNITCTR,UNITDSC)
),CAFTRXHD as (
SELECT * FROM (
VALUES
(1, '2022-01-01','item 1',100,'Item desc 1',1),
(2, '2022-02-01','item 2',200,'Item desc 2',2),
(3, '2022-03-01','item 3',300,'Item desc 3',3),
(4, '2022-01-01','item 1',100,'Item desc 1',1),
(5, '2022-01-01','item 2',100,'Item desc 2',1),
(6, '2022-01-01','item 2',100,'Item desc 2',1),
(7, '2022-02-01','item 2',200,'Item desc 2',2),
(8, '2022-02-01','item 2',200,'Item desc 2',2),
(9, '2022-03-01','item 3',300,'Item desc 3',3),
(10, '2022-03-01','item 3',300,'Item desc 3',3),
(11, '2022-03-01','item 3',300,'Item desc 3',3)
) AS _ (BILLNO,TRXDATE,ITEMCODE,ITEMPRICE,ITEMDESC,EMPID)
),
--Get all item group by Unit
GetAllByUnit as (
SELECT
t.* FROM
(
SELECT
IT.UNITCTR,
IT.UNITDSC,
(SUM(ITEMPRICE) OVER (partition by IT.UNITCTR order by IT.UNITDSC)) as TotalValue
FROM
CAFTRXHD as HD
INNER JOIN STAFF as FF ON HD.EMPID = FF.EMPID
INNER JOIN CAFUNIT as IT ON FF.UNITCTR = IT.UNITCTR
) as t
GROUP BY t.UNITCTR, T.UNITDSC, T.TotalValue
),
--Get all item group by Unit and Item code
GetAllByUnitAndCode as (
SELECT
t.* FROM
(
SELECT
IT.UNITCTR,
IT.UNITDSC,
HD.ITEMCODE,
(SUM(ITEMPRICE) OVER (partition by IT.UNITCTR,HD.ITEMCODE order by IT.UNITDSC)) as TotalValue
FROM
CAFTRXHD as HD
INNER JOIN STAFF as FF ON HD.EMPID = FF.EMPID
INNER JOIN CAFUNIT as IT ON FF.UNITCTR = IT.UNITCTR
) as t
GROUP BY t.UNITCTR, T.UNITDSC, T.ITEMCODE, T.TotalValue
)
SELECT * FROM GetAllByUnit
--SELECT * FROM GetAllByUnitAndCode
The result
UNITCTR UNITDSC TotalValue
U1 Unit 1 400
U2 Unit 2 600
U3 Unit 3 1200
Comment the query last line --SELECT * FROM GetAllByUnit and remove the comment over SELECT * FROM GetAllByUnitAndCode
--SELECT * FROM GetAllByUnit
SELECT * FROM GetAllByUnitAndCode
The result here is top down not only one line
UNITCTR UNITDSC ITEMCODE TotalValue
U1 Unit 1 item 1 200
U1 Unit 1 item 2 200
U2 Unit 2 item 2 600
U3 Unit 3 item 3 1200
Best Regards

How can I check if Table B matches quantity of items that exists in Table A? SQL Server

I have 2 tables, table A and Table B, and I want to check if Table B matches quantity of items that exists on Table A, so, for example:
Table A
S_O
ITEM
QTY
1
ITA
1
3
ITB
2
4
ITC
3
6
ITD
0
Table B
S_O
ITEM
QTY
1
ITA
1
3
ITB
2
4
ITC
3
6
ITD
5
7
ITE
2
8
ITF
1
My first thought was to use an except between the two tables, but then I was asked to check if the quantity was OK or if it was shortage to generate a preview like:
Result from comparing the two tables
S_O
ITEM
STATUS
1
ITA
OK
3
ITB
OK
4
ITC
OK
6
ITD
SHORTAGE
And it needs to ignore items "ITE" and "ITF" because they don't exist in Table A
I'm pretty new with sql server queries, I think I could use a SELECT CASE but I don't know how to do it, I'd appreciate some help in this matter
In those tables my unique identifier is S_O, so it would need to match the S_O, item and quantity for both tables
Here's how you can "pre summarise" table a and b to make item unique, then join:
select
A.item,
A.qty as qtya,
B.qty as qtyb,
A.qty - B.qty as shortageamt,
case
when A.qty = B.qty then 'OK'
else 'Shortage'
end as status
from
(
select item, sum(qty) as qty
from tablea
group by item
) as A
inner join
(
select item, sum(qty) as qty
from tableb
group by item
) as B
on A.item = B.item
You'll only get an item listed in the result if it's in both tables - is that what you want? if ITG is in tablea but not tableb, do you want to see it (with qty 0)? That requires an outer join.
I think you should go with a LEFT JOIN. Also, the first answer does not JOIN on S_O, which you state in your question you need.
"In those tables my unique identifier is S_O, so it would need to match the S_O, item and quantity for both tables"
DECLARE #ta TABLE (S_O INT, ITEM VARCHAR(20), QTY INT)
INSERT INTO #ta
VALUES
(1, 'ITA', 1),
(3, 'ITB', 2),
(4, 'ITC', 3),
(6, 'ITD', 0)
DECLARE #tb TABLE (S_O INT, ITEM VARCHAR(20), QTY INT)
INSERT INTO #tb
VALUES
(1, 'ITA', 1),
(3, 'ITB', 2),
(4, 'ITC', 3),
(6, 'ITD', 5),
(7, 'ITE', 2),
(8, 'ITF', 1);
SELECT ta.S_O,
ta.ITEM,
CASE WHEN ta.ITEM = tb.ITEM AND SUM(ta.QTY) >= SUM(tb.QTY) THEN 'OK' ELSE 'SHORTAGE' END AS 'STATUS'
FROM #ta ta
LEFT JOIN #tb tb ON ta.S_O = tb.S_O
GROUP BY ta.S_O, ta.ITEM, tb.ITEM
S_O
ITEM
STATUS
1
ITA
OK
3
ITB
OK
4
ITC
OK
6
ITD
SHORTAGE

Sum Quantity and Filter Results

I have the following table with order id's and quantities. I need to be able to sum the quantity and retrieve the id's that that equal less than the provided number.
| id | quantity |
|------|----------|
| 100 | 1 |
| 200 | 25 |
| 300 | 15 |
For example, I need the id's where the sum of quantity equals less than 25.
When I try the following it only provides me the first id (100).
Select *
from (
select *,
SUM (Quantity) OVER (ORDER BY Id) AS SumQuantity
from dbo.Orders
) as A
where SumQuantity <= 25
Is it possible to adjust this query where it will provide me id 100 and 300, since the sum total of those orders is less than 25?
I know I can use a where clause on for quantity less than 25, but the important thing here is I need to be able to sum the quantity and pull id's that give me less than the provided number.
Thank you in advance!
Perhaps you want to order by the quantity instead of id?
Select o.*
from (select o.*, SUM (Quantity) OVER (ORDER BY quantity) AS SumQuantity
from dbo.Orders
) o
where SumQuantity <= 25;
This chooses the smallest values so you will get the most rows.
Group by Id and set the condition in the HAVING clause:
select Id, SUM(Quantity) AS SumQuantity
from Orders
group by Id
having SUM(Quantity) <= 25
See the demo.
Results:
Id | SumQuantity
100 | 1
200 | 25
300 | 15
If you want to include all the columns you can modify your query to not ORDER BY id but PARTITION BY id:
select *
from (
select *,
SUM (Quantity) OVER (PARTITION BY Id) AS SumQuantity
from Orders
) as A
where SumQuantity <= 25
For this dataset:
CREATE TABLE Orders([id] varchar(6), [quantity] int);
INSERT INTO Orders([id], [quantity])VALUES
('100', '1'), ('100', '2'),
('200', '25'), ('200', '3'),
('300', '15'), ('300', '5');
Results:
id | quantity | SumQuantity
100 | 1 | 3
100 | 2 | 3
300 | 15 | 20
300 | 5 | 20
See the demo.
Setup:
Your threshold can vary, so let's make it into a variable:
declare #threshold int = 25;
But I also imagine that your table values can vary, like if we add another row only having a quantity of 2:
declare #orders table (id int, quantity int)
insert #orders values (100,1), (200,25), (300,15), (400, 2);
Solution:
For this, we'll need a recursive kind of cross joining:
with
traverse as (
select ids = convert(nvarchar(255), id),
id,
quantity
from #orders
where quantity < #threshold
union all
select ids =
convert(nvarchar(255), tv.ids + ',' +
convert(nvarchar(255), o.id)),
o.id,
quantity = tv.quantity + o.quantity
from traverse tv
cross join #orders o
where tv.id < o.id
and tv.quantity + o.quantity < #threshold
)
select t.ids, t.quantity
from traverse t;
which will produce:
Explanation:
The above code is an algorithm that builds a tree. It starts with your base id's and quantities as nodes (the anchor part of the CTE). It trims anything not meeting the threshold.
It then adds edges by cross joining with orders table again (the recursive part of the CTE), but it only includes the following:
Id's that are greater than the last id considered in the current node (this is so that we avoid duplicate considerations, such as ids = '300,400' and ids = '400,300').
Ids where the sum of quantities is less than the threshold.
Warnings:
But beware, the type of problem you're considering will have computational complexity considerations. But because of the trimming conditions, it will be more efficient than doing all the cross joins first and then filtering the result set at the end.
Also, keep in mind that you may get rows in your table where there is no single set of numbers that will sum up to less than 25. Rather, you can get different paths to that sum. The way I produce the results here will help you identify such a situation.
cross join is perfect for this task, try:
declare #tbl table (id int, quantity int);
insert into #tbl values
(100, 1), (200, 25), (300, 15), (400, 10);
select distinct case when t1.id > t2.id then t1.id else t2.id end,
case when t1.id < t2.id then t1.id else t2.id end
from #tbl t1
cross join #tbl t2
where t1.id <> t2.id
and t1.quantity + t2.quantity < 25

Product Final Price after Many Discount given

I have two tables.
One table of Ids and their prices, and second table of discounts per Id.
In the table of discounts an Id can has many Discounts, and I need to know the final price of an Id.
What is the Best way to query it (in one query) ?
The query should be generic for many discounts per id (not only 2 as mentioned below in the example)
For example
Table one
id price
1 2.00
2 2.00
3 2.00
Table two
id Discount
1 0.20
1 0.30
2 0.40
3 0.50
3 0.60
Final result:
id OrigPrice PriceAfterDiscount
1 2.00 1.12
2 2.00 1.20
3 2.00 0.40
Here's another way to do it:
SELECT T1.ID, T1.Price, T1.Price * EXP(SUM(LOG(1 - T2.Discount)))
FROM T1 INNER JOIN T2 ON T1.ID = T2.ID
GROUP BY T1.ID, T1.Price
The EXP/LOG trick is just another way to do multiplication.
If you have entries in T1 without discounts in T2, you could change the INNER JOIN to a LEFT JOIN. You would end up with the following:
ID Price Discount
4 2.00 NULL
Your logic can either account for the null in the discounted price column and take the original price instead, or just add a 0 discount record for those.
Generally it can be done with a trick with LOG/EXP functions but it is complex.
Here is a basic example:
declare #p table(id int, price money)
declare #d table(id int, discount money)
insert into #p values
(1, 2),
(2, 2),
(3, 2)
insert into #d values
(1, 0.2),
(1, 0.3),
(2, 0.4),
(3, 0.5),
(3, 0.6)
select p.id,
p.price,
p.price * ca.discount as PriceAfterDiscount
from #p p
cross apply (select EXP(SUM(LOG(1 - discount))) as discount FROM #d where id = p.id) ca
For simpler(cursor based approach) you will need a recursive CTE, but in this case you need some unique ordering column in Discounts table to run it correctly. This is shown in #Tanner`s answer.
And finally you can approach this with a regular cursor
I believe this produces the desired results using a CTE to iterate through the discounts. The solution below is re-runnable in isolation.
Edited: to include data that might not have any discounts applied in the output with a left join in the first part of the CTE.
CREATE TABLE #price
(
id INT,
price DECIMAL(5, 2)
);
CREATE TABLE #discount
(
id INT,
discount DECIMAL(5, 2)
);
INSERT INTO #price
(
id,
price
)
VALUES
(1, 2.00),
(2, 2.00),
(3, 2.00),
(4, 3.50); -- no discount on this item
INSERT INTO #discount
(
id,
discount
)
VALUES
(1, 0.20),
(1, 0.30),
(2, 0.40),
(3, 0.50),
(3, 0.60);
-- new temporary table to add a row number to discounts so we can iterate through them
SELECT d.id,
d.discount,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY d.discount) rn
INTO #GroupedDiscount
FROM #discount AS d;
-- note left join in first part of cte to get prices that aren't discounted included
WITH cte
AS (SELECT p.id,
p.price,
CASE
WHEN gd.discount IS NULL THEN
p.price
ELSE
CAST(p.price * (1.0 - gd.discount) AS DECIMAL(5, 2))
END AS discountedPrice,
gd.rn
FROM #price AS p
LEFT JOIN #GroupedDiscount AS gd
ON gd.id = p.id
AND gd.rn = 1
UNION ALL
SELECT cte.id,
cte.price,
CAST(cte.discountedPrice * (1.0 - gd.discount) AS DECIMAL(5, 2)) AS discountedPrice,
cte.rn + 1 AS rn
FROM cte
INNER JOIN #GroupedDiscount AS gd
ON gd.id = cte.id
AND gd.rn = cte.rn + 1
)
SELECT cte.id,
cte.price,
MIN(cte.discountedPrice) AS discountedPrice
FROM cte
GROUP BY id,
cte.price;
DROP TABLE #price;
DROP TABLE #discount;
DROP TABLE #GroupedDiscount;
Results:
id price discountedPrice
1 2.00 1.12
2 2.00 1.20
3 2.00 0.40
4 3.50 3.50 -- no discount
As others have said, EXP(SUM(LOG())) is the way to do the calculation. Here is basically same approach from yet another angle:
WITH CTE_Discount AS
(
SELECT Id, EXP(SUM(LOG(1-Discount))) as TotalDiscount
FROM TableTwo
GROUP BY id
)
SELECT t1.id, CAST(Price * COALESCE(TotalDiscount,1) AS Decimal(18,2)) as FinalPRice
FROM TableOne t1
LEFT JOIN CTE_Discount d ON t1.id = d.id
SQLFIddle Demo

what mysql query should i use to select a category that matches ALL my criteria?

i have the following data in my table called cat_product
cat_id product_id
1 2
1 3
2 2
2 4
If given a set of values for product_id (2,3) i want to know the unique cat_id. In this case, that will be cat_id 1.
how should i construct mysql query?
i tried to use
select distinct cat_id from cat_product where product_id IN (2,3)
but it returns both 1 and 2.
if i use
select distinct cat_id from cat_product where product_id NOT IN (2,3)
i get 2.
is there a better way than
select distinct cat_id from cat_product where product_id IN (2,3)
and cat_id NOT IN
(select distinct cat_id from cat_product where product_id NOT IN (2,3) )
i need to return the category_id that has the EXACT set of product id i am looking for.
basically i have about 10 product ids as input.
SELECT cat_id
FROM (
SELECT DISTINCT cat_id
FROM cat_product
) cpo
WHERE EXISTS
(
SELECT NULL
FROM cat_product cpi
WHERE cpi.cat_id = cpo.cat_id
AND product_id IN (2, 3)
LIMIT 1, 1
)
You need to have a UNIQUE index on (cat_id, product_id) (in this order) for this to work fast.
This solution will use INDEX FOR GROUP BY to get a list of distinct categories, and EXISTS predicate will be a little bit faster than COUNT(*) (since the aggregation requires some overhead).
If you have more than two products to search for, adjust the first argument to LIMIT accordingly.
It should be LIMIT n - 1, 1, where n is the number of items in the IN list.
Update:
To return the categories holding all products from the list and nothing else, use this:
SELECT cat_id
FROM (
SELECT DISTINCT cat_id
FROM cat_product
) cpo
WHERE EXISTS
(
SELECT NULL
FROM cat_product cpi
WHERE cpi.cat_id = cpo.cat_id
AND product_id IN (2, 3)
LIMIT 1, 1
)
AND NOT EXISTS
(
SELECT NULL
FROM cat_product cpi
WHERE cpi.cat_id = cpo.cat_id
AND product_id NOT IN (2, 3)
)
SELECT * FROM
(SELECT cat_id FROM cat_product WHERE product_id=2) a
INNER JOIN
(SELECT cat_id FROM cat_product WHERE product_id=3) b
ON a.cat_id = b.cat_id;
Your own solution would not work (at least not if I understand the question correctly). It will return the id of a category that contains one or more of the listed products and no other products. Try adding the following row to your table, and see if you get the expected result:
insert into cat_product (cat_id, product_id) values (1,5)
If you really need to find the id of a category that has all of the listed products (no matter what other products might be in the category), try this query:
select cat_id
from cat_product
where product_id in (2,3)
group by cat_id
having count(*) = 2
The number 2 on the last line of the query corresponds to the size of the set of products you are searching for. If you are executing the query using some parameterized API, make sure to create an additional parameter bound to productsArray.length or similar.
Find the cat_id's that contains all the given products and nothing else.
Let's create some test data...
create table cat_product (
cat_id int not null,
product_id int not null,
primary key (cat_id, product_id));
delete from cat_product;
insert into cat_product
values
(1, 2),
(1, 3),
(2, 2),
(2, 4),
(3, 1),
(3, 2),
(3, 3),
(3, 4),
(3, 5),
(4, 1),
(4, 2),
(4, 3),
(4, 4),
(4, 6),
(5, 1),
(5, 2),
(5, 3),
(5, 4),
(5, 5),
(5, 6);
Plug in your list of product_id's for cp.product_id IN (1, 2, 3, 4, 5)
AND plug in the number of product_id's on the last line of the query at cats.match_count = 5.
select
cats.cat_id
from
/* Count how many products for each cat match the list of products */
(select
cp.cat_id,
count(*) as match_count
from
cat_product cp
where
cp.product_id IN (1, 2, 3, 4, 5)
group by
cp.cat_id) as cats,
/* Count the number of products in each cat */
(select cat_id, count(*) as cat_count
from cat_product
group by cat_id) as cat_count
where
cats.cat_id = cat_count.cat_id
/* We matched all the products in the cat */
and cats.match_count = cat_count.cat_count
/* We matched all the products we wanted. Without this clause
the query would also match cats that only contain products in
our list (but not all of them) */
and cats.match_count = 5;
Relational Division is what you are looking for