SQL question: excluding records - sql

I have a database (NexusDB (supposedly SQL-92 compliant)) which contains and Item table, a Category table, and a many-to-many ItemCategory table, which is just a pair of keys. As you might expect, Items are assigned to multiple categories.
I am wanting to all the end user to select all items which are
ItemID | CategoryID
--------------------------------
01 | 01
01 | 02
01 | 12
02 | 01
02 | 02
02 | 47
03 | 01
03 | 02
03 | 14
etc...
I want to be able to select all ItemID's that are assigned to Categories X, Y, and Z but NOT assigned to Categories P and Q.
For the example data above, for instance, say I'd like to grab all Items assigned to Categories 01 or 02 but NOT 12 (yielding Items 02 and 03). Something along the lines of:
SELECT ItemID WHERE (CategoryID IN (01, 02))
...and remove from that set SELECT ItemID WHERE NOT (CategoryID = 12)
This is probably a pretty basic SQL question, but it's stumping me at the moment. Any help w/b appreciated.

You could try with EXCEPT
SELECT ItemID FROM Table
EXCEPT
SELECT ItemID FROM Table
WHERE
CategoryID <> 12

I want to be able to select all
ItemID's that are assigned to
Categories X, Y, and Z but NOT
assigned to Categories P and Q.
I can't confirm from the NexusDB documentation on SELECT that they support subqueries, but they do support LEFT OUTER JOIN and GROUP BY. So here's a query that works within these restrictions:
SELECT i1.ItemID
FROM ItemCategory i1
LEFT OUTER JOIN ItemCategory i2
ON (i1.ItemID = i2.ItemID AND i2.CategoryID IN ('P', 'Q'))
WHERE i1.CategoryID IN ('X', 'Y', 'Z')
AND i2.ItemID IS NULL
GROUP BY i1.ItemID
HAVING COUNT(i1.CategoryID) = 3;

SELECT i.ItemID, ic.CategoryID FROM Item AS i
INNER JOIN ItemCategory ic
ON i.ItemID = ic.ItemID
WHERE ic.CategoryId = 1 OR ic.CategoryId = 2
Of course you need to put in the WHERE clause what categories you want to get.

For the simple case that you have with a low and known number of categories you can simply use several joins to check for existence and non-existence:
SELECT
ItemID
FROM
Items I
INNER JOIN ItemCategories IC1 ON IC1.ItemID = I.ItemID AND IC1.CategoryID = '01'
INNER JOIN ItemCategories IC2 ON IC2.ItemID = I.ItemID AND IC2.CategoryID = '02'
LEFT OUTER JOIN ItemCategories IC3 ON IC3.ItemID = I.ItemID AND IC3.CategoryID = '12'
WHERE IC3.ItemID IS NULL
For a more general case, given an unknown number of items in the match and don't match lists, you can use the following query. I've used a table variable (available in SQL Server) for each of the lists, but you can use a select against an actual table or a list of variables/parameters as needed. The idea remains the same:
SELECT
ItemID
FROM
Items I
WHERE
(
SELECT COUNT(*)
FROM ItemCategories IC1
WHERE IC1.ItemID = I.ItemID
AND IC.CategoryID IN
(SELECT CategoryID FROM #MustHaves)
) = (SELECT COUNT(*) FROM #MustHaves) AND
(
SELECT COUNT(*)
FROM ItemCategories IC1
WHERE IC1.ItemID = I.ItemID
AND IC.CategoryID IN
(SELECT COUNT(*) FROM #MustNotHaves)
) = 0

Related

How to left join two tables on specific conditions

I have two tables Price(Type, Values) and Product(Seat) and some values.
Price | Product
-------------+---------
Type Values | Seat
S 4 | FO
P 6 | CA
| FA
I know that [FO] and [CA] belong to type [P], and [FA] belongs to type [S]. How can I join these tables and shows associated type and values:
Results
Seat Type Values
----- ----- -----------
FO P 6
CA P 6
FA S 4
You can join the tables like this:
select pr.seat, sum(p.value)
from price p join
product pr
on pr.seat in ('FO', 'CA') and p.type = 'P' or
pr.seat in ('FA') and p.type = 'S'
group by pr.seat;
That said, you should have a proper table that connects the seats to the products, probably called ProductSeats with one row per product and matching seat.
I would use a derived table to store the mapping between price and seat. This is easily extensible when new requirements come up.
SELECT pri.*, pro.*
FROM price pri
INNER JOIN (
SELECT 'FO' seat, 'P' price
UNION ALL SELECT 'CA' seat, 'P' price
UNION ALL SELECT 'FA' seat, 'S' price
) map ON map.pri = pri.price
INNER JOIN product pro ON pro.seat = map.pro
This can be simplified by using the VALUES() syntax:
SELECT pri.*, pro.*
FROM price pri
INNER JOIN (
VALUES('FO', 'P'), ('CA', 'P'), ('FA', 'S')
) AS map(seat, price) ON map.pri = pri.price
INNER JOIN product pro ON pro.seat = map.pro

Most popular pairs of shops for workers from each company

I've got 2 tables, one with sales and one with companies:
Sales Table
Transaction_Id Shop_id Sale_date Client_ID
92356 24234 11.09.2018 12356
92345 32121 11.09.2018 32121
94323 24321 11.09.2018 21231
94278 45321 11.09.2018 42123
Company table
Client_ID Company_name
12345 ABC
13322 ABC
32321 BCD
22221 BCD
What I want to achieve is distinct count of Clients from each Company for each pair of shops(Clients who had at least 1 transaction in both of shops) :
Shop_Id_1 Shop_id_2 Company_name Count(distinct Client_id)
12356 12345 ABC 31
12345 14278 ABC 23
14323 12345 BCD 32
14278 12345 BCD 43
I think that I have to use self join, but my queries even with filter for one week is killing DB, any thoughts on that? I'm using Microsoft SQL server 2012.
Thanks
I think this is a self-join and aggregation, with a twist. The twist is that you want to include the company in each sales record, so it can be used in the self-join:
with sc as (
select s.*, c.company_name
from sales s join
companies c
on s.client_id = c.client_id
)
select sc1.shop_id, sc2.shop_id, sc1.company_name, count(distinct sc1.client_id)
from sc sc1 join
sc sc2
on sc1.client_id = sc2.client_id and
sc1.company_name = sc2.company_name
group by sc1.shop_id, sc2.shop_id, sc1.company_name;
I think there are some issues with your question. I interpreted it as such that the company table contains the shop ID's, not the ClienId's.
First you can create a solution to get the shops as rows for each company. Here I chose a maximum of 5 shops per company. Don't forget the semicolon in the previous statement before the cte's.
WITH CTE_Comp AS
(
SELECT *, ROW_NUMBER() OVER (PARTITION BY CompanyName ORDER BY ShopID) AS RowNumb
FROM Company AS C
)
SELECT C1.ShopID,
C2.ShopID AS ShopID_2,
C3.ShopID AS ShopID_3,
C4.ShopID AS ShopID_4,
C5.ShopID AS ShopID_5,
C1.CompanyName
INTO ShopsByCompany
FROM CTE_Comp AS C1
LEFT JOIN CTE_Comp AS C2 ON C1.CompanyName= C2.CompanyName AND RowNumb = 2
LEFT JOIN CTE_Comp AS C2 ON C1.CompanyName= C3.CompanyName AND RowNumb = 3
LEFT JOIN CTE_Comp AS C2 ON C1.CompanyName= C4.CompanyName AND RowNumb = 4
LEFT JOIN CTE_Comp AS C2 ON C1.CompanyName= C5.CompanyName AND RowNumb = 5
WHERE C1.RowNumb = 1
After that, in a few steps, I think you could get the desired result:
WITH ClientsPerShop AS
(
SELECT ShopID,
COUNT (DISTINCT ClientID) AS TotalClients
FROM Sales
GROUP BY ShopID
)
, ClienstsPerCompany AS
(
SELECT CompanyName,
SUM (TotalClients) AS ClientsPerComp
FROM Company AS C
INNER JOIN ClientsPerShop AS CPS ON C.ShopID = CPS.ShopID
GROUP BY CompanyName
)
SELECT *
FROM ClienstsPerCompany AS CPA
INNER JOIN ShopsByCompany AS SBC ON SBC.CompanyName = CPA.CompanyName
Hopefully this will bring you closer to your solution, best of luck!

Oracle SQL: SQL join with group by (count) and having clauses

I have 3 tables
Table 1.) Sale
Table 2.) ItemsSale
Table 3.) Items
Table 1 and 2 have ID in common and table 2 and 3 have ITEMS in common.
I'm having trouble with a query that I have made so far but can't seem to get it right.
I'm trying to select all the rows that only have one row and match a certain criteria here is my query:
select *
from sales i
inner join itemssales j on i.id = j.id
inner join item u on j.item = u.item
where u.code = ANY ('TEST','HI') and
i.created_date between TO_DATE('1/4/2016 12:00:00 AM','MM/DD/YYYY HH:MI:SS AM') and
TO_DATE('1/4/2016 11:59:59 PM','MM/DD/YYYY HH:MI:SS PM')
group by i.id
having count(i.id) = 1
In the ItemSale table there are two entries but in the sale table there is only one. This is fine...but I need to construct a query that will only return to me the one record.
I believe the issue is with the "ANY" portion, the query only returns one row and that row is the record that doesn't meet the "ANY ('TEST', 'HI')" criteria.
But in reality that record with that particular ID has two records in ItemSales.
I need to only return the records that legitimately only have one record.
Any help is appreciated.
--EDIT:
COL1 | ID
-----|-----
2 | 26
3 | 85
1 | 23
1 | 88
1 | 6
1 | 85
What I also do is group them and make sure the count is equal to 1 but as you can see, the ID 85 is appearing here as one record which is a false positive because there is actually two records in the itemsales table.
I even tried changing my query to j.id after the select since j is the table with the two records but no go.
--- EDIT
Sale table contains:
ID
---
85
Itemsales table contains:
ID | Position | item_id
---|----------|---------
85 | 1 | 6
85 | 2 | 7
Items table contains:
item_id | code
--------|------
7 | HI
6 | BOOP
The record it is returning is the one with the Code of 'BOOP'
Thanks,
"I need to only return the records that legitimately only have one record."
I interpret this to mean, you only want to return SALES with only one ITEM. Furthermore you need that ITEM to meet your additional criteria.
Here's one approach, which will work fine with small(-ish) amounts of data but may not scale well. Without proper table descriptions and data profiles it's not possible to offer a performative solution.
with itmsal as
( select sales.id
from itemsales
join sales on sales.id = itemsales.id
where sales.created_date >= date '2016-01-04'
and sales.created_date < date '2016-01-05'
group by sales.id having count(*) = 1)
select sales.*
, item.*
from itmsal
join sales on sales.id = itmsal.id
join itemsales on itemsales.id = itmsal.id
join items on itemsales.item = itemsales.item
where items.code in ('TEST','HI')
I think you are trying to restrict the results so that items MUST ONLY have the code of 'TEST' or 'HI'.
select
sales.*
from (
select
s.id
from Sales s
inner join Itemsales itss on s.id = itss.id
inner join Items i on itss.item_id = i.item_id
group by
s.id
where s.created_date >= date '2016-01-04'
and s.created_date < date '2016-01-05'
having
sum(case when i.code IN('TEST','HI') then 0 else 1 end) = 0
) x
inner join sales on x.id = sales.id
... /* more here as required */
This construct only returns sales.id that have items with ONLY those 2 codes.
Note it could be done with a common table expression (CTE) but I prefer to only use those when there is an advantage in doing so - which I do not see here.
If I get it correctly this may work (not tested):
select *
from sales s
inner join (
select i.id, count( i.id ) as cnt
from sales i
inner join itemssales j on i.id = j.id
inner join item u on j.item = u.item and u.code IN ('TEST','HI')
where i.created_date between TO_DATE('1/4/2016 12:00:00 AM','MM/DD/YYYY HH:MI:SS AM') and
TO_DATE('1/4/2016 11:59:59 PM','MM/DD/YYYY HH:MI:SS PM')
group by i.id
) sj on s.id = sj.id and sj.cnt = 1

SQL Server - only join if condition is met

I have three tables (at least, something similar) with the following relationships:
Item table:
ID | Val
---------+---------
1 | 12
2 | 5
3 | 22
Group table:
ID | Parent | Range
---------+---------+---------
1 | NULL | [10-30]
2 | 1 | [20-25]
3 | NULL | [0-15]
GroupToItem table:
GroupID | ItemID
---------+---------
1 | 1
1 | 3
And now I want to add rows to the GroupToItem table for Groups 2 and 3, using the same query (since some other conditions not shown here are more complicated). I want to restrict the items through which I search if the new group has a parent, but to look through all items if there is not.
At the moment I am using an IF/ELSE on two statements that are almost exactly the same, but for the addition of another JOIN row when a parent exists. Is it possible to do a join to reduce the number of items to look at, only if a restriction is possible?
My two queries as they stand are given below:
DECLARE #GroupID INT = 2;...
INSERT INTO GroupToItem(GroupID, ItemID)
SELECT g.ID,
i.ID,
FROM Group g
JOIN Item i ON i.Val IN g.Range
JOIN GroupToItem gti ON g.Parent = gti.GroupID AND i.ID = gti.ItemID
WHERE g.ID = #GroupID
-
DECLARE #GroupID INT = 3;...
INSERT INTO GroupToItem(GroupID, ItemID)
SELECT g.ID,
i.ID,
FROM Group g
JOIN Item i ON i.Val IN g.Range
WHERE g.ID = #GroupID
So essentially I only want to do the second JOIN if the given group has a parent. Is this possible in a single query? It is important that the number of items that are compared against the range is as small as possible, since for me this is an intensive operation.
EDIT: This seems to have solved it in this test setup, similar to what was suggested by Denis Valeev. I'll accept if I can get it to work with my live data. I've been having some weird issues - potentially more questions coming up.
SELECT g.Id,
i.Id
FROM Group g
JOIN Item i ON (i.Val > g.Start AND i.Val < g.End)
WHERE g.Id = 2
AND (
(g.ParentId IS NULL)
OR
(EXISTS(SELECT 1 FROM GroupToItem gti WHERE g.ParentId = gti.GroupId AND i.Id = gti.ItemId))
)
SQL Fiddle
Try this:
INSERT INTO GroupToItem(GroupID, ItemID)
SELECT g.ID,
i.ID,
FROM Group g
JOIN Item i ON i.Val IN g.Range
WHERE g.ID = #GroupID
and (g.ID in (3) or exists (select top 1 1 from GroupToItem gti where g.Parent = gti.GroupID AND i.ID = gti.ItemID))
If a Range column is a varchar datatype, you can try something like this:
INSERT INTO GROUPTOITEM (GROUPID, ITEMID)
SELECT A.ID, B.ID
FROM GROUP AS A
LEFT JOIN ITEM AS B
ON B.VAL BETWEEN CAST(SUBSTRING(SUBSTRING(A.RANGE,1,CHARINDEX('-',A.RANGE,1)-1),2,10) AS INT)
AND CAST(REPLACE(SUBSTRING(A.RANGE,CHARINDEX('-',A.RANGE,1)+1,10),']','') AS INT)

How to find all the products with specific multi attribute values

I am using postgresql.
I have a table called custom_field_answers. The data looks like this
Id | product_id | value | number_value |
4 | 2 | | 117 |
3 | 1 | | 107 |
2 | 1 | bangle | |
1 | 2 | necklace | |
I want to find all the products which has text_value as 'bangle' and number_value less than 50.
Here was my first attempt.
SELECT "products".* FROM "products" INNER JOIN "custom_field_answers"
ON "custom_field_answers"."product_id" = "products"."id"
WHERE ("custom_field_answers"."value" ILIKE 'bangle')
Here is my second attempt.
SELECT "products".* FROM "products" INNER JOIN "custom_field_answers"
ON "custom_field_answers"."product_id" = "products"."id"
where ("custom_field_answers"."number_value" < 50)
Here is my final attempt.
SELECT "products".* FROM "products" INNER JOIN "custom_field_answers"
ON "custom_field_answers"."product_id" = "products"."id"
WHERE ("custom_field_answers"."value" ILIKE 'bangle')
AND ("custom_field_answers"."number_value" < 50)
but this does not select any product record.
A WHERE clause can only look at columns from one row at a time.
So if you need a condition that applies to two different rows from a table, you need to join to that table twice, so you can get columns from both rows.
SELECT p.*
FROM "products" AS p
INNER JOIN "custom_field_answers" AS a1 ON p."id" = a1."product_id"
INNER JOIN "custom_field_answers" AS a2 ON p."id" = a1."product_id"
WHERE a1."value" = 'bangle' AND a2."number_value" < 50
It produces no records because there is no custom_field_answers record that meets both criteria. What you want is a list of product_ids that have the necessary records in the table. Just in case no one gets to writing the SQL for you, and until I have a chance to work it out myself, I thought I would at least explain to you why your query is not working.
This should work:
SELECT p.* FROM products LEFT JOIN custom_field_answers c
ON (c.product_id = p.id AND c.value LIKE '%bangle%' AND c.number_value
Hope it helps
Your bangle-related number_value fields are null, so you won't be able to do a straight comparison in those cases. Instead, convert your nulls to 0s first.
SELECT "products".* FROM "products" INNER JOIN "custom_field_answers"
ON "custom_field_answers"."product_id" = "products"."id"
WHERE ("custom_field_answers"."value" LIKE '%bangle%')
AND (coalesce("custom_field_answers"."number_value", 0) < 50)
Didn't actually test it, but this general idea should work:
SELECT *
FROM products
WHERE
EXISTS (
SELECT *
FROM custom_field_answers
WHERE
custom_field_answers.product_id = products.id
AND value = 'bangle'
)
AND EXISTS (
SELECT *
FROM custom_field_answers
WHERE
custom_field_answers.product_id = products.id
AND number_value < 5
)
In plain English: Get all products such that...
there is a related row in custom_field_answers where value = 'bangle'
and there is (possibly different) related row in custom_field_answers where number_value < 5.