SQL Server 2008, how to check if multi records exist in the DB? - sql

I have 3 tables:
recipe:
id, name
ingredient:
id, name
recipeingredient:
id, recipeId, ingredientId, quantity
Every time, a customer creates a new recipe, I need to check the recipeingredient table to verify if this recipe exists or not. If ingredientId and quantity are exactly the same, I will tell the customer the recipe already exists. Since I need to check multiple rows, need help to write this query.

Knowing your ingredients and quantities, you can do something like this:
select recipeId as ExistingRecipeID
from recipeingredient
where (ingredientId = 1 and quantity = 1)
or (ingredientId = 8 and quantity = 1)
or (ingredientId = 13 and quantity = 1)
group by recipeId
having count(*) = 3 --must match # of ingeredients in WHERE clause

I originally thought that the following query would find pairs of recipes that have exactly the same ingredients:
select ri1.recipeId, ri2.recipeId
from RecipeIngredient ri1 full outer join
RecipeIngredient ri2
on ri1.ingredientId = ri2.ingredientId and
ri1.quantity = ri2.quantity and
ri1.recipeId < ri2.recipeId
group by ri1.recipeId, ri2.recipeId
having count(ri1.id) = count(ri2.id) and -- same number of ingredients
count(ri1.id) = count(*) and -- all r1 ingredients are present
count(*) = count(ri2.id) -- all r2 ingredents are present
However, this query doesn't count things correctly, because the mismatches don't have the right pairs of ids. Alas.
The following does do the correct comparison. It counts the ingredients in each recipe before the join, so this value can just be compared on all matching rows.
select ri1.recipeId, ri2.recipeId
from (select ri.*, COUNT(*) over (partition by recipeid) as numingredients
from #RecipeIngredient ri
) ri1 full outer join
(select ri.*, COUNT(*) over (partition by recipeid) as numingredients
from #RecipeIngredient ri
) ri2
on ri1.ingredientId = ri2.ingredientId and
ri1.quantity = ri2.quantity and
ri1.recipeId < ri2.recipeId
group by ri1.recipeId, ri2.recipeId
having max(ri1.numingredients) = max(ri2.numingredients) and
max(ri1.numingredients) = count(*)
The having clause guarantees that each recipe that the same number of ingredients, and that the number of matching ingredients is the total. This time, I've tested it on the following data:
insert into #recipeingredient select 1, 1, 1
insert into #recipeingredient select 1, 2, 10
insert into #recipeingredient select 2, 1, 1
insert into #recipeingredient select 2, 2, 10
insert into #recipeingredient select 2, 3, 10
insert into #recipeingredient select 3, 1, 1
insert into #recipeingredient select 4, 1, 1
insert into #recipeingredient select 4, 3, 10
insert into #recipeingredient select 5, 1, 1
insert into #recipeingredient select 5, 2, 10
If you have a new recipe, you can modify this query to just look for the recipe in one of the tables (say ri1) using an additional condition on the on clause.
If you place the ingredients in a temporary table, you can substitute one of these tables, say ri1, with the new table.

You might try something like this to find if you have a duplicate:
-- Setup test data
declare #recipeingredient table (
id int not null primary key identity
, recipeId int not null
, ingredientId int not null
, quantity int not null
)
insert into #recipeingredient select 1, 1, 1
insert into #recipeingredient select 1, 2, 10
insert into #recipeingredient select 2, 1, 1
insert into #recipeingredient select 2, 2, 10
-- Actual Query
if exists (
select *
from #recipeingredient old
full outer join #recipeingredient new
on old.recipeId != new.recipeId -- Different recipes
and old.ingredientId = new.ingredientId -- but same ingredients
and old.quantity = new.quantity -- and same quantities
where old.id is null -- Match not found
or new.id is null -- Match not found
)
begin
select cast(0 as bit) as IsDuplicateRecipe
end
else begin
select cast(1 as bit) as IsDuplicateRecipe
end
Since this is really only searching for a duplicate, you might want to substitute a temp table or pass a table variable for the "new" table. This way you wouldn't have to insert the new records before doing your search. You could also insert into the base tables, wrap the whole thing in a transaction and rollback based upon the results.

Related

sql join using recursive cte

Edit: Added another case scenario in the notes and updated the sample attachment.
I am trying to write a sql to get an output attached with this question along with sample data.
There are two table, one with distinct ID's (pk) with their current flag.
another with Active ID (fk to the pk from the first table) and Inactive ID (fk to the pk from the first table)
Final output should return two columns, first column consist of all distinct ID's from the first table and second column should contain Active ID from the 2nd table.
Below is the sql:
IF OBJECT_ID('tempdb..#main') IS NOT NULL DROP TABLE #main;
IF OBJECT_ID('tempdb..#merges') IS NOT NULL DROP TABLE #merges
IF OBJECT_ID('tempdb..#final') IS NOT NULL DROP TABLE #final
SELECT DISTINCT id,
current
INTO #main
FROM tb_ID t1
--get list of all active_id and inactive_id
SELECT DISTINCT active_id,
inactive_id,
Update_dt
INTO #merges
FROM tb_merges
-- Combine where the id from the main table matched to the inactive_id (should return all the rows from #main)
SELECT id,
active_id AS merged_to_id
INTO #final
FROM (SELECT t1.*,
t2.active_id,
Update_dt ,
Row_number()
OVER (
partition BY id, active_id
ORDER BY Update_dt DESC) AS rn
FROM #main t1
LEFT JOIN #merges t2
ON t1.id = t2.inactive_id) t3
WHERE rn = 1
SELECT *
FROM #final
This sql partially works. It doesn't work, where the id was once active then gets inactive.
Please note:
the active ID should return the last most active ID
the ID which doesn't have any active ID should either be null or the ID itself
ID where the current = 0, in those cases active ID should be the ID current in tb_ID
ID's may get interchanged. For example there are two ID's 6 and 7, when 6 is active 7 is inactive and vice versa. the only way to know the most current active state is by the update date
Attached sample might be easy to understand
Looks like I might have to use recursive cte for achieiving the results. Can someone please help?
thank you for your time!
I think you're correct that a recursive CTE looks like a good solution for this. I'm not entirely certain that I've understood exactly what you're asking for, particularly with regard to the update_dt column, just because the data is a little abstract as-is, but I've taken a stab at it, and it does seem to work with your sample data. The comments explain what's going on.
declare #tb_id table (id bigint, [current] bit);
declare #tb_merges table (active_id bigint, inactive_id bigint, update_dt datetime2);
insert #tb_id values
-- Sample data from the question.
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(5, 0),
-- A few additional data to illustrate a deeper search.
(6, 1),
(7, 1),
(8, 1),
(9, 1),
(10, 1);
insert #tb_merges values
-- Sample data from the question.
(3, 1, '2017-01-11T13:09:00'),
(1, 2, '2017-01-11T13:07:00'),
(5, 4, '2013-12-31T14:37:00'),
(4, 5, '2013-01-18T15:43:00'),
-- A few additional data to illustrate a deeper search.
(6, 7, getdate()),
(7, 8, getdate()),
(8, 9, getdate()),
(9, 10, getdate());
if object_id('tempdb..#ValidMerge') is not null
drop table #ValidMerge;
-- Get the subset of merge records whose active_id identifies a "current" id and
-- rank by date so we can consider only the latest merge record for each active_id.
with ValidMergeCTE as
(
select
M.active_id,
M.inactive_id,
[Priority] = row_number() over (partition by M.active_id order by M.update_dt desc)
from
#tb_merges M
inner join #tb_id I on M.active_id = I.id
where
I.[current] = 1
)
select
active_id,
inactive_id
into
#ValidMerge
from
ValidMergeCTE
where
[Priority] = 1;
-- Here's the recursive CTE, which draws on the subset of merges identified above.
with SearchCTE as
(
-- Base case: any record whose active_id is not used as an inactive_id is an endpoint.
select
M.active_id,
M.inactive_id,
Depth = 0
from
#ValidMerge M
where
not exists (select 1 from #ValidMerge M2 where M.active_id = M2.inactive_id)
-- Recursive case: look for records whose active_id matches the inactive_id of a previously
-- identified record.
union all
select
S.active_id,
M.inactive_id,
Depth = S.Depth + 1
from
#ValidMerge M
inner join SearchCTE S on M.active_id = S.inactive_id
)
select
I.id,
S.active_id
from
#tb_id I
left join SearchCTE S on I.id = S.inactive_id;
Results:
id active_id
------------------
1 3
2 3
3 NULL
4 NULL
5 4
6 NULL
7 6
8 6
9 6
10 6

SQL ALL IN clause

I have been searching for this, but didn't find anything special.
Is it possible to have an SQL query which will act like ALL IN? To better explain, Here is a table structure.
Orders table
OrderItem table (having several columns, but mainly ProductID, OrderID)
ProductGroup table (several columns, but mainly GroupID and ProductID)
I want to write a query which will select all those order which belongs to a specific ProductGroup. So if I have a group named "XYZ" with ID = 10. It has One ProductID in it. Say ProductID01
An order came in with two order items. ProductID01 and ProductID02. To find all orders in the specific Product Group I can use a simple SQL like
SELECT bvc_OrderItem.ProductID, bvc_OrderItem.OrderID
From bvc_OrderItem
INNER JOIN bvc_Product_Group_Product with (nolock) ON bvc_OrderItem.ProductID = bvc_Product_Group_Product.ProductID
WHERE bvc_Product_Group_Product.GroupID = 10
Or I can write using an IN Clause
SELECT bvc_OrderItem.ProductID, bvc_OrderItem.OrderID
From bvc_OrderItem
WHERE ProductID IN (
SELECT ProductID FROM bvc_Product_Group_Product WHERE GroupID=10
)
However, This will return all orders where one or more ProductIDs are part of the product group. I need to return the order row ONLY if ALL of the order items are part of the Product Group
So basically, I need an IN Clause which will considered matched if ALL of the values inside IN Clause matches the rows in bvc_OrderItem.
Or if we are using the Join, then the Join should only succeed if ALL rows on the left have values in the corresponding right table.
If I could write it more simply, I would write it like this
Select ID FROM Table WHERE ID IN (1, 2, 3, 4)
and if the table contains all rows with ids 1,2,3,4; it should return success. If any of these IN values is missing, it should return false and nothing should be selected.
Do you think it is possible? Or there is a workaround to do that?
You can get the list of orders in a variety of ways, such as:
SELECT oi.OrderID
FROM bvc_OrderItem oi JOIN
bvc_Product_Group_Product pgp
ON oi.ProductID = pgp.ProductId AND
pgp.GroupID = 10
GROUP BY oi.OrderID
HAVING COUNT(DISTINCT oi.ProductID) = (SELECT COUNT(*)
FROM bvc_Product_Group_Product
WHERE GroupID = 10
);
Getting the specific products requires an additional join. In most cases, the list of orders is more useful.
The problem with your ALL IN syntax is that it doesn't do what you want. You want to select orders. The syntax:
SELECT bvc_OrderItem.ProductID, bvc_OrderItem.OrderID
From bvc_OrderItem
WHERE ProductID ALL IN (SELECT ProductID
FROM bvc_Product_Group_Product
WHERE GroupID = 10
)
This doesn't specify that you intend for the grouping to be by OrderId, as opposed to some other level.
More fundamentally, though, the SQL language is inspired by relational algebra. The constructs of SELECT, JOIN, WHERE, and GROUP BY directly relate to relational algebra fundamental constructs. The notion of ALL IN -- although sometimes useful -- can be expressed using the more basic building blocks.
You can do it by this tricky statement:
DECLARE #Items TABLE
(
OrderID INT ,
ProductID INT
)
DECLARE #Groups TABLE
(
ProductID INT ,
GroupID INT
)
INSERT INTO #Items
VALUES ( 1, 1 ),
( 1, 2 ),
( 2, 1 ),
( 3, 3 ),
( 3, 4 )
INSERT INTO #Groups
VALUES ( 1, 10 ),
( 2, 10 ),
( 3, 10 ),
( 4, 15 )
SELECT OrderID
FROM #Items i
GROUP BY OrderID
HAVING ( CASE WHEN 10 = ALL ( SELECT gg.GroupID
FROM #Items ii
JOIN #Groups gg ON gg.ProductID = ii.ProductID
WHERE ii.OrderID = i.OrderID ) THEN 1
ELSE 0
END ) = 1
Output:
OrderID
1
2
Also(this is better):
SELECT OrderID
FROM #Items i
JOIN #Groups g ON g.ProductID = i.ProductID
GROUP BY OrderID
HAVING MIN(g.GroupID) = 10
AND MAX(g.GroupID) = 10

Where Not Exists With One to Many Relationship

I am having a hard time figuring out how to ask this question, so I'll just go straight to the example code. Let's say I have these tables:
create table Item
(
ItemId int identity(1,1),
Name nvarchar(256)
)
create table ItemSale
(
ItemSaleId int identity(1,1),
ItemId int,
Price decimal,
CategoryId tinyint
)
What I want to retrieve is the list of ItemSale records that are not in a given CategoryId. The complication, at least for me, is that if a record exists in ItemSale for a given Item, I do not want to see any records for that Item.
So if I have this data:
insert into Item(Name)
select N'Widget' union all
select N'Foo' union all
select N'Buzz'
insert into ItemSale(ItemId, Price, CategoryId)
select 1, 9.95, 1 union all
select 1, 19.95, 2 union all
select 3, 99.99, 3
And the CategoryId I want to filter out is 1, then I don't want to see any records for ItemId 1 ("Widget"). So, with that sample data, I would only see the ItemSale record for Item with ID 3.
I know that my solution will most likely involve some sort of NOT EXISTS OR LEFT JOIN but I'm struggling with how to filter out all records instead of just the specific record that matches my criteria. What am I missing?
SQLFiddle: http://sqlfiddle.com/#!3/79c58
I might be over simplifying your problem, but I think this would work:
SELECT *
FROM ItemSale i
WHERE NOT EXISTS
( SELECT 1
FROM ItemSale i2
WHERE i.ItemID = i2.ItemID
AND i2.CategoryID = 1
);
Example on SQL Fiddle

sql select a field into 2 columns

I am trying to run below 2 queries on the same table and hoping to get results in 2 different columns.
Query 1: select ID as M from table where field = 1
returns:
1
2
3
Query 2: select ID as N from table where field = 2
returns:
4
5
6
My goal is to get
Column1 - Column2
-----------------
1 4
2 5
3 6
Any suggestions? I am using SQL Server 2008 R2
Thanks
There has to be a primary key to foreign key relationship to JOIN data between two tables.
That is the idea about relational algebra and normalization. Otherwise, the correlation of the data is meaningless.
http://en.wikipedia.org/wiki/Database_normalization
The CROSS JOIN will give you all possibilities. (1,4), (1,5), (1, 6) ... (3,6). I do not think that is what you want.
You can always use a ROW_NUMBER() OVER () function to generate a surrogate key in both tables. Order the data the way you want inside the OVER () clause. However, this is still not in any Normal form.
In short. Why do this?
Quick test database. Stores products from sporting goods and home goods using non-normal form.
The results of the SELECT do not mean anything.
-- Just play
use tempdb;
go
-- Drop table
if object_id('abnormal_form') > 0
drop table abnormal_form
go
-- Create table
create table abnormal_form
(
Id int,
Category int,
Name varchar(50)
);
-- Load store products
insert into abnormal_form values
(1, 1, 'Bike'),
(2, 1, 'Bat'),
(3, 1, 'Ball'),
(4, 2, 'Pot'),
(5, 2, 'Pan'),
(6, 2, 'Spoon');
-- Sporting Goods
select * from abnormal_form where Category = 1
-- Home Goods
select * from abnormal_form where Category = 2
-- Does not mean anything to me
select Id1, Id2 from
(select ROW_NUMBER () OVER (ORDER BY ID) AS Rid1, Id as Id1
from abnormal_form where Category = 1) as s
join
(select ROW_NUMBER () OVER (ORDER BY ID) AS Rid2, Id as Id2
from abnormal_form where Category = 2) as h
on s.Rid1 = h.Rid2
We definitely need more information from the user.

Access 97 Outer join issue

I have two tables I want to join.
Table A has one column, named "Week", and contains 52 rows: 1,2,3,4,5,6 etc.
Table 2 has three columns, named "Name", "Week", and "Total", and contains 10 rows:
'Bob', 1, 1
'Bob', 3, 1
'Joe', 4, 1
'Bob', 6, 1
I want to join these together so that my data looks like:
NAME|WEEK|TOTAL
'Bob', 1, 1
'Bob', 2, 0
'Bob', 3, 1
'Bob', 4, 0
'Bob', 5, 0
'Bob', 6, 1
As you can see, a simple outer join. However, when I try to do this, I'm not getting the expected result, no matter what kind of join I use.
My query below:
SELECT a.WEEK, b.Total
FROM Weeks a LEFT JOIN Totals b ON (a.Week = b.Week and b.Name ='Bob')
The result of this query is
NAME|WEEK|TOTAL
'Bob', 1, 1
'Bob', 3, 1
'Bob', 6, 1
Thanks in advance for the help!
I know its access but your join is incorrect. Here we go in sql server..same concept just look at the join condition:
--dont worry about this code im just creating some temp tables
--table to store one column (mainly week number 1,2..52)
CREATE TABLE #Weeks
(
weeknumber int
)
--insert some test data
--week numbers...I'll insert some for you
INSERT INTO #Weeks(weeknumber) VALUES(1)
INSERT INTO #Weeks(weeknumber) VALUES(2)
INSERT INTO #Weeks(weeknumber) VALUES(3)
INSERT INTO #Weeks(weeknumber) VALUES(4)
INSERT INTO #Weeks(weeknumber) VALUES(5)
INSERT INTO #Weeks(weeknumber) VALUES(6)
--create another table with two columns storing the week # and a total for that week
CREATE TABLE #Table2
(
weeknumber int,
total int
)
--insert some data
INSERT INTO #Table2(weeknumber, total) VALUES(1, 100)
--notice i skipped week 2 on purpose to show you the results
INSERT INTO #Table2(weeknumber, total) VALUES(3, 100)
--here's the magic
SELECT t1.weeknumber as weeknumber, ISNULL(t2.total,0) as total FROM
#Weeks t1 LEFT JOIN #Table2 t2 ON t1.weeknumber=t2.weeknumber
--get rid of the temp tables
DROP TABLE #table2
DROP TABLE #Weeks
Results:
1 100
2 0
3 100
4 0
5 0
6 0
Take your week number table (the table that has one column:
SELECT t1.weeknumber as weeknumber
Add to it a null check to replace the null value with a 0. I think there is something in access like ISNULL:
ISNULL(t2.total, 0) as total
And start your join from your first table and left join to your second table on the weeknumber field. The result is simple:
SELECT t1.weeknumber as weeknumber, ISNULL(t2.total,0) as total FROM
#Weeks t1 LEFT JOIN #Table2 t2 ON t1.weeknumber=t2.weeknumber
Do not pay attention to all the other code I have posted, that is only there to create temp tables and insert values into the tables.
SELECT b.Name, b.Week, b.Total
FROM Totals AS b
WHERE b.Name ='Bob'
UNION
SELECT 'Bob' AS Name, a.Week, 0 AS Total
FROM Weeks AS a
WHERE NOT EXISTS ( SELECT *
FROM Totals AS b
WHERE a.Week = b.Week
AND b.Name ='Bob' );
You were on the right track, but just needed to use a left join. Also the NZ function will put a 0 if total is null.
SELECT Totals.Person, Weeks.WeekNo, Nz(Totals.total, 0) as TotalAmount
FROM Weeks LEFT JOIN Totals
ON (Weeks.WeekNo = Totals.weekno and Totals.Person = 'Bob');
EDIT: The query you now have won't even give the results you've shown because you left out the Name field (Which is a bad name for a field because it is a reserved word.). You're still not providing all the information. This query works.
*Another Approach: * Create a separate query on the Totals table having a where clause: Name = 'Bob'
Select Name, WeekNo, Total From Totals Where Name = 'Bob';
and substitute that query for the Totals table in this query.
Select b.Name, w.WeekNo, b.total
from Weeks as w
LEFT JOIN qryJustBob as b
on .WeekNo = b.WeekNo;