SQL: Ensuring all rows are checked against a set of rows - sql

I have two tables that look like this:
Supplier
SN SNAME Stat City
+----------+-------+----+--------+
| S1 | Smith | 20 | London |
| S2 | Jones | 10 | Paris |
| S3 | Blake | 30 | Paris |
| S4 | Clark | 20 | London |
| S5 | Adams | 30 | Athens |
+----------+-------+----+--------+
Shipment
SN PN QTY
+----+----+-----+
| S1 | P1 | 300 |
| S1 | P2 | 200 |
| S1 | P3 | 400 |
| S1 | P4 | 200 |
| S1 | P5 | 100 |
| S1 | P6 | 100 |
| S2 | P1 | 300 |
| S2 | P2 | 400 |
| S3 | P2 | 200 |
| S4 | P2 | 200 |
| S4 | P4 | 300 |
| S4 | P5 | 400 |
+----+----+-----+
I have the find the following:
List the supplier names (SNAME) for those suppliers who ship at least all those parts (PN) supplied by supplier S2.
In other words, I need to list all those suppliers who ship at least P1 and P2, although obviously my query needs to be focused more on the question at hand.
I'm pretty sure I have to use some form of NOT EXISTS -- perhaps the double not exists, to achieve this. I tried doing a self-join of the Shipment table to itself -- but I can't understand how to get it beyond checking for ANY item that appears in the PN for S2 and instead check to make sure ALL parts for S2 are in the list before including the name in the result.

I used a Common Table Expression to accomplish this:
--CTE will contain records from the Supplier you are looking to check against
WITH desiredSN AS
(
SELECT s.SN, s.PN
FROM Shipment AS s
WHERE s.SN = 'S2'
)
--SELECT will join all other shippers with each record of your Supplier S2 here
SELECT s.SN
FROM Shipment AS s
INNER JOIN desiredSN AS d ON s.PN = d.PN
WHERE s.SN != 'S2'
GROUP BY s.SN
--Having ensures that the count of your old supplier matches
--the number a potential supplier overlaps with
HAVING COUNT(1) =
(
SELECT COUNT(1)
FROM desiredSN AS d
)
If you do not want to use the CTE, you can use subqueries instead:
SELECT s.SN
FROM Shipment AS s
INNER JOIN
(
SELECT s.SN, s.PN
FROM Shipment AS s
WHERE s.SN = 'S2'
)
AS d ON s.PN = d.PN
WHERE s.SN != 'S2'
GROUP BY s.SN
HAVING COUNT(1) =
(
SELECT COUNT(1)
FROM Shipment AS s
WHERE s.SN = 'S2'
)

Could you not use a nested select statement to refine your results?
For example:
SELECT SNAME FROM Supplier
INNER JOIN Shipment ON Supplier.SN = Shipment.SN
WHERE Shipment.PN IN (SELECT PN FROM Shipment WHERE SN = S2)
This way, the parts (PN) are restricted to just those that are sold by S2.
edited changed the statement to
Shipment.PN IN (SELECT PN FROM Shipment WHERE SN = S2)
Now, even if they sold one of everything, they'll only be returned if they sold at least what S2 sells.

Related

SQL Optimization, Nested Query on the same table

i have to create a query that return employees having mutliple territories parent for the same function code :
Table employee_territory_function
employee_id| employee_function_id | territory_id
+----------+-----------------------+-------------+
| 12345 | C1 | t1 |
| 12345 | C1 | t2 |
| 12346 | C2 | t3 |
| 12346 | C2 | t4 |
| 12347 | C4 | t8 |
Table territory
territory_id| territory_parent_id
+-----------+-------------------+
| t1 | P1 |
| t2 | P1 |
| t3 | P2 |
| t4 | P3 |
| t8 | P8 |
the result must be the employee_id 12346 which have multiple parents
my query was :
select * from employee_territory_function tr1 where tr1.employee_id in (
select ee.employee_id from (
select et.employee_id from employee_territory_function et
join territory territory on territory.id = et.territory_id
where et.employee_id in (
select etf.employee_id ,etf.employee_function_id from employee_territory_function etf
group by etf.employee_id ,etf.employee_function_id having count(*)>1)) ee
group by ee.employee_id ,ee.employee_function_id ,ee.territory_parent_id having count(*) =1)
The query takes much time execution with 10k for the couple ( employee , function code )
is there a way to optimize or rewrite the query differently ?
SELECT E.EMPLOYEE_ID,E.EMPLOYEE_FUNCTION_ID
FROM EMPLOYEE AS E
JOIN TERRITORY AS T ON E.TERRITORY_ID=T.TERRITORY_ID
GROUP BY E.EMPLOYEE_ID,E.EMPLOYEE_FUNCTION_ID
HAVING MIN(T.TERRITORY_PARENT_ID)<>MAX(T.TERRITORY_PARENT_ID)
Based on your sample data

Can someone help me figure out if I'm making a mistake in my query?

I'm trying to create a query that returns the names of all people in my database that have less than half of the money of the person with the most money.
These is my query:
select P1.name
from Persons P1 left join
AccountOf A1 on A1.person_id = P1.id left join
BankAccounts B1 on B1.id = A1.account_id
group by name
having SUM(B1.balance) < MAX((select SUM(B1.balance) as b
from AccountOf A1 left join
BankAccounts B1 on B1.id = A1.account_id
group by A1.person_id
order by b desc
LIMIT 1)) * 0.5
This is the result:
+-------+
| name |
+-------+
| Evert |
+-------+
I have the following tables in the database:
+---------+--------+--+
| Persons | | |
+---------+--------+--+
| id | name | |
| 11 | Evert | |
| 12 | Xavi | |
| 13 | Ludwig | |
| 14 | Ziggy | |
+---------+--------+--+
+--------------+---------+
| BankAccounts | |
+--------------+---------+
| id | balance |
| 11 | 525000 |
| 12 | 750000 |
| 13 | 1900000 |
| 14 | 1600000 |
+--------------+---------+
+-----------+-----------+------------+
| AccountOf | | |
+-----------+-----------+------------+
| id | person_id | account_id |
| 301 | 11 | 12 |
| 302 | 13 | 12 |
| 303 | 13 | 14 |
| 304 | 14 | 11 |
| 305 | 14 | 13 |
+-----------+-----------+------------+
What am I missing here? I should get two entries in the result (Evert, Xavi)
I wouldn't approach the logic this way (I would use window functions). But your final having has two levels of aggregation. That shouldn't work. You want:
having SUM(B1.balance) < (select 0.5 * SUM(B1.balance) as b
from AccountOf A1 join
BankAccounts B1 on B1.id = A1.account_id
group by A1.person_id
order by b desc
limit 1
)
I also moved the 0.5 into the subquery and changed the left join to a join -- the tables need to match to get balances.
I would recommend window functions, if your - undisclosed! - database supports them.
You can join and aggregate just once, and then use a window max() to get the top balance. All that is then left to is to filter in an outer query:
select *
fom (
select p.id, p.name, coalesce(sum(balance), 0) balance,
max(sum(balance)) over() max_balance
from persons p
left join accountof ao on ao.person_id = p.id
left join bankaccounts ba on ba.id = ao.account_id
group by p.id, p.name
) t
where balance > max_balance * 0.5

Join three tables based on one key, putting data into same column

I have three tables that I am trying to join together to check that the proper data matches. I have table A which is a list of all accounts that a commission was paid on and what that commission amount was. I have Table B and Table C which are two tables that have commission calculations in it. The goal is to compare Table A to Table and to Table C and pulling back the amounts from both tables to ensure a match. The part I am struggling with is, Table A has all the accounts that are the base population. Table B has some and Table C as some. An account will be in either Table B or C, but never in both. I want to pull the payment from Table A, and then verify to the payment in Table B or C(whichever it occurs) and the same with commission. I then am doing a case when that compares the two fields and tells me if it matches are not.
+---------+---------+-----+------+
| Table A | | | |
+---------+---------+-----+------+
| Account | Uniq_ID | Pay | Comm |
| 12345 | ABCD | 100 | 10 |
| 23456 | OLPOL | 25 | 2 |
| 45678 | LKJHG | 200 | 15 |
| 96385 | LKJ67 | 250 | 26 |
+---------+---------+-----+------+
+---------+---------+-----+------+
| Table B | | | |
+---------+---------+-----+------+
| Account | Uniq_ID | Pay | Comm |
| 12345 | ABCD | 100 | 8 |
| 45678 | LKJHG | 200 | 15 |
+---------+---------+-----+------+
+---------+---------+-----+------+
| Table C | | | |
+---------+---------+-----+------+
| Account | Uniq_ID | Pay | Comm |
| 23456 | OLPOL | 25 | 2 |
| 96385 | LKJ67 | 250 | 32 |
+---------+---------+-----+------+
I am trying to get my results to show up in a columns called pay_ver and comm_verf, and it would populate with the data from either Table B or C based on which it matched with. I am hoping to have to output look like so....
+---------+---------+-----+----------+------+-----------+---------+
| Output | | | | | | |
+---------+---------+-----+----------+------+-----------+---------+
| Account | Uniq_ID | Pay | Pay_verf | comm | comm_Verf | Matched |
| 12345 | ABCD | 100 | 100 | 10 | 8 | No |
| 23456 | OLPOL | 25 | 25 | 2 | 2 | Yes |
| 45678 | LKJHG | 200 | 200 | 15 | 15 | Yes |
| 96385 | LKJ67 | 250 | 250 | 26 | 32 | No |
+---------+---------+-----+----------+------+-----------+---------+
This is the code I have used to join Table A to B, and Table A to C but I have done this in two separate queries giving me two outputs. I would like to be able to do this in one, so I only have one output.
select a.account, a.uniq_id, a.pay, b.pay as pay_verf, a.comm, b.comm as comm_verf,
CASE WHEN a.comm = b.comm THEN 'MATCHED'
ELSE 'UNMATCHED'
END as Matched
from tblA a
left join tblB b
on a.account = b.account
and a.uniq_id = b.uniq_id;
I can not just figure out how to also get it to join to Table C without adding an extra column.
You can do:
select
account, uniq_id, pay,
pay_total as pay_verf,
comm,
comm - comm_total as comm_verf,
case when comm = comm_total then 'Yes' else 'No' end as matched
from (
select
a.account, a.uniq_id, a.pay, a.comm,
coalesce(b.pay, 0) + coalesce(c.pay, 0) as pay_total,
coalesce(b.comm, 0) + coalesce(c.comm, 0) as comm_total
from table_a a
left join table_b b on a.account = b.account
left join table_c c on a.account = c.account
) x
You are very close. Just need to add one more join and an addition WHEN to your case statement. This should act like an if elseif else logic. So it checks if a.comm = b.comm and then checks a.comm = c.comm. If neither match if will set to unmatched. This works well because you stated the ID can't be in both B and C.
select a.account, a.uniq_id, a.pay, b.pay as pay_verf, a.comm, b.comm as comm_verf,
CASE WHEN a.comm = b.comm THEN 'MATCHED'
WHEN a.comm = c.comm THEN 'MATCHED'
ELSE 'UNMATCHED'
END as Matched
from tblA a
left join tblB b
on a.account = b.account
and a.uniq_id = b.uniq_id;
left join tblB c
on a.account = c.account
and a.uniq_id = c.uniq_id;
Yet another option could be something like
SELECT a.account, a.uniq_id, a.pay, bc.pay as pay_verf, a.comm, bc.comm as comm_verf
FROM a left join (
SELECT * from b
UNION ALL
SELECT * from c
) bc on (a.account = bc.account and a.uniq_id = bc.uniq_id)

SQL statement count all items per order, display itemid, orderid and itemNumber

First, thanks for your time and your help!
I have one single table with "product_ID" and "order_ID".
One Order has 0 or multiple(n) products.
"product_ID" is unique, because each product has its own barcode.
| product_ID | order_ID |
+---------------------+------------------+
| p1 | o1 |
| p2 | o1 |
| p3 | o2 |
| p4 | o3 |
| p5 | o2 |
| p6 | o4 |
| p7 | o1 |
My problem is the following: I want to count how often one order occurs and display the number in one additional column.
I have tried it for about 2 days, but I cannot seem to solve this problem.
One of my attempts with T-SQL, but suggestions of other SQL versions are also appreciated:
SELECT
order_ID, COUNT(order_ID) as 'product_COUNT'
FROM
product_table
GROUP BY
order_ID
UNION ALL
SELECT
product_ID, COUNT(*)
FROM
product_table
GROUP BY
product_ID
The result should look like this:
| product_ID | Order_ID | product_Count |
+---------------------+------------------+------------------+
| p1 | o1 | 3 |
| p2 | o1 | 3 |
| p3 | o2 | 2 |
| p4 | o3 | 1 |
| p5 | o2 | 2 |
| p6 | o4 | 1 |
| p7 | o1 | 3 |
You seem to want window functions:
select po.*, count(*) over (partition by order_id) as product_count
from single_table po;
Your sample query has aggregation, but that doesn't seem to be necessary.
Another solution, without using window functions
DECLARE #MyTable TABLE (product_ID VARCHAR(5), order_ID VARCHAR(5))
INSERT INTO #MyTable VALUES
('p1', 'o1'),
('p2', 'o1'),
('p3', 'o2'),
('p4', 'o3'),
('p5', 'o2'),
('p6', 'o4'),
('p7', 'o1')
SELECT T.*, T1.product_Count FROM #MyTable T
LEFT JOIN ( SELECT order_ID, COUNT(*) product_Count FROM #MyTable GROUP BY order_ID) T1 ON T.order_ID = T1.order_ID
Result:
product_ID order_ID product_Count
---------- -------- -------------
p1 o1 3
p2 o1 3
p3 o2 2
p4 o3 1
p5 o2 2
p6 o4 1
p7 o1 3

Get a count of rows not matching criteria

Given a simple table (order detail/history kind of thing) with a customer and a product:
+--------------------+
| customer | product |
+--------------------+
| Smith | p1 |
| Smith | p3 |
| Jones | p1 |
| Jones | p2 |
| Davis | p3 |
| Davis | p9 |
| Brown | p1 |
| Brown | p2 |
| Brown | p5 |
+----------+---------+
I want to list all customers that have never ordered product p1, i.e. Davis in the above data set.
This is where i started but, of course, it doesnt work and I can't think of where to go next:
select
customer,
count(*) as c
where product='p1'
and c = 0
Try this out:
select customer
from MyTable
where customer not in (select customer from MyTable where Product = 'P1')
Here is one way, using an aggregation query:
select customer
from t
group by customer
having sum(case when product = 'p1' then 1 else 0 end) = 0
This gives you all the customers in the table. If you have a separate list of customers, then you might use:
select customer
from customerTable
where customer not in (select customer from t where product = 'p1')
You can also use this approach
SELECT t.CustomerName
FROM Table t
WHERE NOT EXISTS (SELECT 1
FROM Table t2
WHERE t2.CustomerName = t.CustomerName
AND t2.ProductName = 'p1')