Generating all possible combinations of a timetable using an SQL Query - sql

I have an awkward SQL Puzzle that has bested me.
I am trying to generate a list of possible configurations of student blocks so I can fit their course choices into a timetable. A list of possible qualifications and blocks for a student could be as the following:
Biology A
Biology C
Biology D
Biology E
Chemistry B
Chemistry C
Chemistry D
Chemistry E
Chemistry F
Computing D
Computing F
Tutorial A
Tutorial B
Tutorial E
A possible solution of blocks for a student could be
Biology D
Chemistry C
Computing F
Tutorial E
How would I query the above dataset to produce all possible combinations of lessons and blocks for a student? I could then pare down the list removing the ones that clash and choose one that works. I estimate that in this instance there will be about 120 combinations in total.
I could imagine that it would be some kind of cross join. I have tried all sorts of solutions using window functions and cross apply etc but they have all had some kind of flaw. They all tend to get tripped up because each student has a different number of courses and each course has a different number of blocks.
Cheers for any help you can offer! I can paste in the gnarled mess of a query I have if necessary too!
Alex

For a fixed number of qualifications, the answer is relatively simple - the CROSS JOIN option from the previous answers will work perfectly.
However, if the number of qualifications is unknown, or likely to change in the future, hard-coding four CROSS JOIN operations won't work. In this case, the answer gets more complicated.
For small numbers of rows, you could use a variation of this answer on DBA, which uses powers of two and bit comparisons to generate the combinations. However, this will be limited to a very small number of rows.
For larger numbers of rows, you can use a function to generate every combination of 'M' numbers from 'N' rows. You can then join this back to a ROW_NUMBER value computed on your source data to get the original row.
The function to generate the combinations could be written in TSQL, but it would make more sense to use SQLCLR if possible:
[SqlFunction(
DataAccess = DataAccessKind.None,
SystemDataAccess = SystemDataAccessKind.None,
IsDeterministic = true,
IsPrecise = true,
FillRowMethodName = "FillRow",
TableDefinition = "CombinationId bigint, Value int"
)]
public static IEnumerable Combinations(SqlInt32 TotalCount, SqlInt32 ItemsToPick)
{
if (TotalCount.IsNull || ItemsToPick.IsNull) yield break;
int totalCount = TotalCount.Value;
int itemsToPick = ItemsToPick.Value;
if (0 >= totalCount || 0 >= itemsToPick) yield break;
long combinationId = 1;
var result = new int[itemsToPick];
var stack = new Stack<int>();
stack.Push(0);
while (stack.Count > 0)
{
int index = stack.Count - 1;
int value = stack.Pop();
while (value < totalCount)
{
result[index++] = value++;
stack.Push(value);
if (index == itemsToPick)
{
for (int i = 0; i < result.Length; i++)
{
yield return new KeyValuePair<long, int>(
combinationId, result[i]);
}
combinationId++;
break;
}
}
}
}
public static void FillRow(object row, out long CombinationId, out int Value)
{
var pair = (KeyValuePair<long, int>)row;
CombinationId = pair.Key;
Value = pair.Value;
}
(Based on this function.)
Once the function is in place, generating the list of valid combinations is fairly easy:
DECLARE #Blocks TABLE
(
Qualification varchar(10) NOT NULL,
Block char(1) NOT NULL,
UNIQUE (Qualification, Block)
);
INSERT INTO #Blocks
VALUES
('Biology', 'A'),
('Biology', 'C'),
('Biology', 'D'),
('Biology', 'E'),
('Chemistry', 'B'),
('Chemistry', 'C'),
('Chemistry', 'D'),
('Chemistry', 'E'),
('Chemistry', 'F'),
('Computing', 'D'),
('Computing', 'F'),
('Tutorial', 'A'),
('Tutorial', 'B'),
('Tutorial', 'E')
;
DECLARE #Count int, #QualificationCount int;
SELECT
#Count = Count(1),
#QualificationCount = Count(DISTINCT Qualification)
FROM
#Blocks
;
WITH cteNumberedBlocks As
(
SELECT
ROW_NUMBER() OVER (ORDER BY Qualification, Block) - 1 As RowNumber,
Qualification,
Block
FROM
#Blocks
),
cteAllCombinations As
(
SELECT
C.CombinationId,
B.Qualification,
B.Block
FROM
dbo.Combinations(#Count, #QualificationCount) As C
INNER JOIN cteNumberedBlocks As B
ON B.RowNumber = C.Value
),
cteMatchingCombinations As
(
SELECT
CombinationId
FROM
cteAllCombinations
GROUP BY
CombinationId
HAVING
Count(DISTINCT Qualification) = #QualificationCount
And
Count(DISTINCT Block) = #QualificationCount
)
SELECT
DENSE_RANK() OVER(ORDER BY C.CombinationId) As CombinationNumber,
C.Qualification,
C.Block
FROM
cteAllCombinations As C
INNER JOIN cteMatchingCombinations As MC
ON MC.CombinationId = C.CombinationId
ORDER BY
CombinationNumber,
Qualification
;
This query will generate a list of 172 rows representing the 43 valid combinations:
1 Biology A
1 Chemistry B
1 Computing D
1 Tutorial E
2 Biology A
2 Chemistry B
2 Computing F
2 Tutorial E
...
In case you need the TSQL version of the Combinations function:
CREATE FUNCTION dbo.Combinations
(
#TotalCount int,
#ItemsToPick int
)
Returns #Result TABLE
(
CombinationId bigint NOT NULL,
ItemNumber int NOT NULL,
Unique (CombinationId, ItemNumber)
)
As
BEGIN
DECLARE #CombinationId bigint;
DECLARE #StackPointer int, #Index int, #Value int;
DECLARE #Stack TABLE
(
ID int NOT NULL Primary Key,
Value int NOT NULL
);
DECLARE #Temp TABLE
(
ID int NOT NULL Primary Key,
Value int NOT NULL Unique
);
SET #CombinationId = 1;
SET #StackPointer = 1;
INSERT INTO #Stack (ID, Value) VALUES (1, 0);
WHILE #StackPointer > 0
BEGIN
SET #Index = #StackPointer - 1;
DELETE FROM #Temp WHERE ID >= #Index;
-- Pop:
SELECT #Value = Value FROM #Stack WHERE ID = #StackPointer;
DELETE FROM #Stack WHERE ID = #StackPointer;
SET #StackPointer -= 1;
WHILE #Value < #TotalCount
BEGIN
INSERT INTO #Temp (ID, Value) VALUES (#Index, #Value);
SET #Index += 1;
SET #Value += 1;
-- Push:
SET #StackPointer += 1;
INSERT INTO #Stack (ID, Value) VALUES (#StackPointer, #Value);
If #Index = #ItemsToPick
BEGIN
INSERT INTO #Result (CombinationId, ItemNumber)
SELECT #CombinationId, Value
FROM #Temp;
SET #CombinationId += 1;
SET #Value = #TotalCount;
END;
END;
END;
Return;
END
It's virtually the same as the SQLCLR version, except for the fact that TSQL doesn't have stacks or arrays, so I've had to fake them with table variables.

One giant cross join?
select * from tablea,tableb,tablec,tabled
That will actually work for what you need, where tablea is the biology entries, b is chem, c is computing and d is tutorial. You can specify the joins a bit better:
select * from tablea cross join tableb cross join tablec cross join tabled.
Technically both statement are the same...this is all cross join so the comma version above is simpler, in more complicated queries, you'll want to use the second statement so you can be very explicit as to where you are cross joining vs inner/left joins.
You can replace the 'table' entries with a select union statement to give the values you are looking for in query form:
select * from
(select 'biology' as 'course','a' as 'class' union all select 'biology','c' union all select 'biology','d' union all select 'biology','e') a cross join
(select 'Chemistry' as 'course','b' as 'class' union all select 'Chemistry','c' union all select 'Chemistry','d' union all select 'Chemistry','e' union all select 'Chemistry','f') b cross join
(select 'Computing' as 'course','a' as 'class' union all select 'Computing','c') c cross join
(select 'Tutorial ' as 'course','a' as 'class' union all select 'Tutorial ','b' union all select 'Tutorial ','e') d
There is your 120 results (4*5*3*2)

Not really seeing the problem, but does this sqlFiddle work?

You should be able to do with a simple union, however each select of the union would have a filter on only one class type so you don't get
BIO, BIO, BIO, BIO, BIO
BIO, CHEM, BIO, BIO, BIO
etc...
select
b.course as BioCourse,
c.course as ChemCourse,
co.course as CompCourse,
t.course as Tutorial
from
YourTable b,
YourTable c,
YourTable co,
YourTable t
where
b.course like 'Biology%'
AND c.course like 'Chemistry%'
AND co.course like 'Computing%'
AND t.course like 'Tutorial%'

Lets use the paradigm where Table1 is Biology, Table2 is chemistry, Table3 is computing and Table4 is tutorial. Each table has 1 column and that is the possible blocks for that table or course. To get all possible combinations, we want to Cartesian Product all of the tables together and then filter the rows out that have duplicate letters.
Each column in the end result will represent their respective course. This means column 1 in the finished table will be the block letter for Biology which is Table1.
So the SQL for the answer would look something like this.
SELECT * FROM Table1,Table2,Table3,Table4
WHERE col1 != col2
AND col1 != col3
AND col1 != col4
AND col2 != col3
AND col2 != col4
AND col3 != col4;
Note: This is trivial to extend to the case where each table has 2 columns, the first is the subject and the 2nd is the block. Substitutions just need to be done in the where clause but if I ignore this case the code is much easier to follow along.
This is a little verbose but this works if each student must have a class from each of the tables and the max number of classes is 4 classes. This solution breaks down if a student doesnt have to have 4 classes.
The exact SQL needed may be a little different depending on what database your using. For example != could be <>.
Hope this helps!

Related

How do i create a DB2 UNION query using variables from a list

So i have a union query like:
select count(id)
from table 1
where membernumber = 'x'
and castnumber = 'y'
union
select count(id)
from table 1
where membernumber = 'x'
and castnumber = 'y'
union
etc...
There will be over 200 unions coming from a list 2x 200 table with values for x and y in each row. So each union query has to get the value of x and y from the corresponding row (not in any particular order).
How can i achieve that ?
Thanks
Try this:
DECLARE GLOBAL TEMPORARY TABLE
SESSION.PARAMETERS
(
MEMBERNUMBER INT
, CASTNUMBER INT
) DEFINITION ONLY WITH REPLACE
ON COMMIT PRESERVE ROWS NOT LOGGED;
-- Insert all the the constants in your application with
INSERT INTO SESSION.PARAMETERS
(MEMBERNUMBER, CASTNUMBER)
VALUES (?, ?);
-- I don't know the meaning of the result you want to get
-- but it's equivalent
select distinct count(t.id)
from table1 t
join session.parameters p
on p.membernumber = t.membernumber
and p.castnumber = t.castnumber
group by t.membernumber, t.castnumber;

Find matching sets in a database table

I have a junction table in a (SQL Server 2014) database with columns FirstID and SecondID. Given a specific FirstID, I'd like to find all other FirstIDs from the table that have an equivalent set of SecondIDs (even if that set is empty).
Sample Data:
FirstId SecondId
1 1
1 2
2 3
3 1
3 2
... ...
In the case of the sample data, if I specified FirstID = 1, then I'd expect 3 to appear in the result set.
I've tried the following so far, which works pretty well except for empty sets:
SELECT FirstSecondEqualSet.FirstId
FROM FirstSecond FirstSecondOriginal
INNER JOIN FirstSecond FirstSecondEqualSet ON FirstSecondOriginal.SecondId = FirstSecondEqualSet.SecondId
WHERE FirstSecondOriginal.FirstId = #FirstId 
AND FirstSecondEqualSet.FirstId != #FirstId
GROUP BY FirstSecondEqualSet.FirstId
HAVING COUNT(1) = (SELECT COUNT(1) FROM FirstSecond WHERE FirstSecond.FirstId = #FirstId)
I think it's somehow related to Relational Division with no Remainder (RDNR). See this great article by Dwain Camps for reference.
DECLARE #firstId INT = 1
SELECT
f2.FirstId
FROM FirstSecond f1
INNER JOIN FirstSecond f2
ON f2.SecondId = f1.SecondId
AND f1.FirstId <> f2.FirstId
WHERE
f1.FirstId = #firstId
GROUP BY f2.FirstId
HAVING COUNT(*) = (SELECT COUNT(*) FROM FirstSecond WHERE FirstId = #firstId)
Here is one approach. It counts the number of values for each firstid and then joins on the secondid.
select fs2.firstid
from (select fs1.*, count(*) over (partition by firstid) as numseconds
from firstsecond fs1
where fs1.firstid = #firstid
) fs1 join
(select fs2.*, count(*) over (partition by firstid) as numseconds
from firstsecond fs2
) fs2
on fs1.secondid = fs2.secondid and fs1.numseconds = fs2.numseconds
group by fs2.firstid
having count(*) = max(fs1.numseconds);

SQL: I want a row to be return with NULL even if there is no match to my IN clause

I would like my SQL query to return a row even if there is no row matching in my IN clause.
For exemple this query:
SELECT id, foo
FROM table
WHERE id IN (0, 1, 2, 3)
would return:
id|foo
0|bar
1|bar
2|bar
3|null
But instead I have (because no row with id 3):
id|foo
0|bar
1|bar
2|bar
I have been able to find this trick:
SELECT tmpTable.id, table.bar
FROM (
SELECT 0 as id
UNION SELECT 1
UNION SELECT 2
UNION SELECT 3
) tmpTable
LEFT JOIN
(
SELECT table.foo, table.id
FROM table
WHERE table.id IN (0, 1, 2, 3)
) table
on table.id = tmpTable.id
Is there a better way?
Bonus: How to make it work with myBatis's list variable?
overslacked is right. Most SQL developers use an auxiliary table that stores integers (and one that stores dates). This is outlined in an entire chapter of Joe Celko's "SQL for Smarties".
Example:
CREATE TABLE numeri ( numero INTEGER PRIMARY KEY )
DECLARE #x INTEGER
SET #x = 0
WHILE #x < 1000
BEGIN
INSERT INTO numeri ( numero ) VALUES ( #x )
SET #x = #x + 1
END
SELECT
numero AS id,
foo
FROM
numeri
LEFT OUTER JOIN my_table
ON my_table.id = numero
WHERE
numero BETWEEN 0 AND 3
Main Goal of Programming minimal code high performance no need this things just remove id 3 from in clause
What about just saying:
SELECT id, foo
FROM table
WHERE id >= 0 AND <= 3

Select column value that matches a combination of other columns values on the same table

I have a table called Ads and another Table called AdDetails to store the details of each Ad in a Property / Value style, Here is a simplified example with dummy code:
[AdDetailID], [AdID], [PropertyName], [PropertyValue]
2 28 Color Red
3 28 Speed 100
4 27 Color Red
5 28 Fuel Petrol
6 27 Speed 70
How to select Ads that matches many combinations of PropertyName and PropertyValue, for example :
where PropertyName='Color' and PropertyValue='Red'
And
where PropertyName='Speed' and CAST(PropertyValue AS INT) > 60
You are probably going to do stuff like this a lot so I would start out by making a view that collapses all of the properties to a single row.
create view vDetail
as
select AdID,
max(case PropertyName
when 'Color' then PropertyValue end) as Color,
cast(max(case PropertyName
when 'Speed' then PropertyValue end) as Int) as Speed,
max(case PropertyName
when 'Fuel' then PropertyValue end) as Fuel
from AdDetails
group by AdID
This approach also solves the problem with casting Speed to an int.
Then if I select * from vDetails
This makes it easy to deal with when joined to the parent table. You said you needed a variable number of "matches" - note the where clause below. #MatchesNeeded would be the count of the number of variables that were not null.
select *
from Ads a
inner join vDetails v
on a.AdID = v.AdID
where case when v.Color = #Color then 1 else 0 end +
case when v.Spead > #Speed then 1 else 0 end +
case when v.Fuel = #Fuel then 1 else 0 end = #MatchesNeeded
I think you have two main problems to solve here.
1) You need to be able to CAST varchar values to integers where some values won't be integers.
If you were using SQL 2012, you could use TRY_CAST() ( sql server - check to see if cast is possible ). Since you are using SQL 2008, you will need a combination of CASE and ISNUMERIC().
2) You need an efficient way to check for the existence of multiple properties.
I often see a combination of joins and where clauses for this, but I think this can quickly get messy as the number of properties that you check gets over... say one. Instead, using an EXISTS clause tends to be neater and I think it provides better clues to the SQL Optimizer instead.
SELECT AdID
FROM Ads
WHERE 1 = 1
AND EXISTS (
SELECT 1
FROM AdDetails
WHERE AdID = Ads.AdID
AND ( PropertyName='Color' and PropertyValue='Red' )
)
AND EXISTS (
SELECT 1
FROM AdDetails
WHERE AdID = Ads.AdID
AND PropertyName='Speed'
AND
(
CASE
WHEN ISNUMERIC(PropertyValue) = 1
THEN CAST(PropertyValue AS INT)
ELSE 0
END
)
> 60
)
You can add as many EXISTS clauses as you need without the query getting particularly difficult to read.
Something like this might work for 2 conditions, you would have to adapt depending on the number of conditions
select a.*
from ads as a
join addetails as d1 on d1.adid = a.id
join addetails as d2 on d2.adid = a.id
where (d1.PropertyName='Color' and d1.PropertyValue='Red')
and (d2.PropertyName='Speed' and d2.CAST(PropertyValue AS INT) > 60)
DECLARE #AdDetails TABLE
(
AdDetailID INT,
AdID INT,
PropertyName VARCHAR(20),
PropertyValue VARCHAR(20)
)
INSERT INTO #AdDetails
( AdDetailID, AdID, PropertyName, PropertyValue )
VALUES
(2, 28, 'Color', 'Red'),
(3, 28, 'Speed', '100'),
(4, 27, 'Color', 'Red'),
(5, 28, 'Fuel', 'Petrol'),
(6, 27, 'Speed', '70');
--Col1
DECLARE #ColorValue VARCHAR(20) = 'Red'
--Col2
DECLARE #SpeedValue INT = 90
DECLARE #SpeedType VARCHAR(2) = '>'
--Col3
DECLARE #FuelValue VARCHAR(20) = null
SELECT DISTINCT a.AdID FROM #AdDetails a
INNER JOIN
(
SELECT *
FROM #AdDetails
WHERE #ColorValue IS NULL
OR #ColorValue = PropertyValue
) Color
ON Color.AdID = a.AdID
INNER JOIN
(
SELECT *
FROM #AdDetails
WHERE #SpeedType IS NULL
UNION
SELECT *
FROM #AdDetails
WHERE PropertyName = 'Speed'
AND ((#SpeedType = '>' AND CONVERT(INT, PropertyValue) > #SpeedValue)
OR (#SpeedType = '<' AND CONVERT(INT, PropertyValue) < #SpeedValue)
OR (#SpeedType = '=' AND CONVERT(INT, PropertyValue) = #SpeedValue))
) AS Speed
ON Speed.AdID = a.AdID
INNER JOIN
(
SELECT *
FROM #AdDetails
WHERE #FuelValue IS NULL
OR (#FuelValue = PropertyValue)
) AS Fuel
ON Fuel.AdID = a.AdID
I add one inner join clause per property type (with some overrides), your sql query would pass all of the possible property type info in one go nulling out whatever they don't want. very ugly code though as it grows.

SQL: Return "true" if list of records exists?

An alternative title might be:
Check for existence of multiple rows?
Using a combination of SQL and C# I want a method to return true if all products in a list exist in a table. If it can be done all in SQL that would be preferable. I have written a method that returns whether a single productID exists using the following SQL:
SELECT productID FROM Products WHERE ProductID = #productID
If this returns a row, then the c# method returns true, false otherwise.
Now I'm wondering if I have a list of product IDs (not a huge list mind you, normally under 20). How can I write a query that will return a row if all the product id's exist and no row if one or more product id's does not exist?
(Maybe something involving "IN" like:
SELECT * FROM Products WHERE ProductID IN ('1', '10', '100', 'ABC'))
EDIT:
How the result is expressed is not important to me. Whether the query returns a 1 or 0, an empty resultset or a non-empty one, true or false doesn't matter. I'd prefer the answer that is 1) easy to read and understand and 2) performant
I was envisioning concatenating the list of product id's with the SQL. Obviously this opens the code up to SQL injection (the product id's are actually varchar. in this case the chance is slim but still want to avoid that possibility). So if there is a way around this that would be better. Using SQL Server 2005.
Product ID's are varchar
Here's how I usually do it:
Just replace your query with this statement SELECT * FROM table WHERE 1
SELECT
CASE WHEN EXISTS
(
SELECT * FROM table WHERE 1
)
THEN 'TRUE'
ELSE 'FALSE'
END
Given your updated question, these are the simplest forms:
If ProductID is unique you want
SELECT COUNT(*) FROM Products WHERE ProductID IN (1, 10, 100)
and then check that result against 3, the number of products you're querying (this last part can be done in SQL, but it may be easier to do it in C# unless you're doing even more in SQL).
If ProductID is not unique it is
SELECT COUNT(DISTINCT ProductID) FROM Products WHERE ProductID IN (1, 10, 100)
When the question was thought to require returning rows when all ProductIds are present and none otherwise:
SELECT ProductId FROM Products WHERE ProductID IN (1, 10, 100) AND ((SELECT COUNT(*) FROM Products WHERE ProductID IN (1, 10, 100))=3)
or
SELECT ProductId FROM Products WHERE ProductID IN (1, 10, 100) AND ((SELECT COUNT(DISTINCT ProductID) FROM Products WHERE ProductID IN (1, 10, 100))=3)
if you actually intend to do something with the results. Otherwise the simple SELECT 1 WHERE (SELECT ...)=3 will do as other answers have stated or implied.
#Mark Hurd, thanks for pointing out the error.
this will work (if you are using Postgresql, Sql Server 2008):
create table products
(
product_id int not null
);
insert into products values(1),(2),(10),(100);
SELECT
CASE
WHEN EXISTS(
SELECT 1
FROM (values(1),(10),(100)) as x(id)
WHERE x.id NOT IN (select product_id from products))
THEN 0 --'NOT ALL'
ELSE 1 -- 'ALL'
END
If you are using MySQL, make a temporary memory table(then populate 1,10,100 there):
create table product_memory(product_id int) engine=MEMORY;
insert into product_memory values(1),(10),(100);
SELECT
CASE
WHEN EXISTS(
SELECT 1
FROM product_memory
WHERE product_memory.id NOT IN (select product_id from products))
THEN 0 -- 'NOT ALL'
ELSE 1 -- 'ALL'
END
On your C# code:
bool isAllExist = (int)(new SqlCommand(queryHere).ExecuteScalar()) == 1;
[EDIT]
How can I write a query that will
return a row if all the product id's
exist and no row if one or more
product id's does not exist?
Regarding, returning a row(singular) if all rows exists, and no row to be returned if one or more product id does not exists:
MySql:
SELECT 1
WHERE
NOT EXISTS(
SELECT 1
FROM product_memory
WHERE product_memory.id NOT IN (select product_id from products) )
Posgresql, Sql Server 2008:
SELECT 1
WHERE
NOT EXISTS(
SELECT 1
FROM (values(1),(10),(100)) as x(id)
WHERE x.id NOT IN (select product_id from products) )
Then on your C# code:
var da = new SqlDataAdapter(queryhere, connectionhere);
var dt = new DataTable();
da.Fill(dt);
if (dt.Rows.Count > 0)
return true;
else
return false;
Or just make the condition shorter:
return dt.Rows.Count > 0;
Assuming you're using SQL Server, the boolean type doesn't exist, but the bit type does, which can hold only 0 or 1 where 0 represents False, and 1 represents True.
I would go this way:
select 1
from Products
where ProductId IN (1, 10, 100)
Here, a null or no row will be returned (if no row exists).
Or even:
select case when EXISTS (
select 1
from Products
where ProductId IN (1, 10, 100)
) then 1 else 0 end as [ProductExists]
Here, either of the scalar values 1 or 0 will always be returned (if no row exists).
DECLARE #values TABLE (ProductId int)
INSERT #values (1)
INSERT #values (10)
INSERT #values (100)
SELECT CASE WHEN (SELECT COUNT(*) FROM #values v) =
(SELECT COUNT(*) FROM Products p WHERE p.ProductId IN
(SELECT v.ProductId FROM #values v))
THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END [AreAllFound]
I know this is old but I think this will help anyone else who comes looking...
SELECT CAST(COUNT(ProductID) AS bit) AS [EXISTS] FROM Products WHERE(ProductID = #ProductID)
This will ALWAYS return TRUE if exists and FALSE if it doesn't (as opposed to no row).
You can use a SELECT CASE statement like so:
select case when EXISTS (
select 1
from <table>
where <condition>
) then TRUE else FALSE end
It returns TRUE when your query in the parents exists.
For PostgreSQL:
SELECT COUNT(*) = 1 FROM (
SELECT 1 FROM $table WHERE $condition LIMIT 1
) AS t
// not familiar with C#, but C#'s equivalent of PHP's:
$count = count($productIds); // where $productIds is the array you also use in IN (...)
SELECT IF ((SELECT COUNT(*) FROM Products WHERE ProductID IN (1, 10, 100)) = $count, 1, 0)
If the IN clause is a parameter (either to SP or hot-built SQL), then this can always be done:
SELECT (SELECT COUNT(1)
FROM product_a
WHERE product_id IN (1, 8, 100)
) = (number of commas in product_id as constant)
If the IN clause is a table, then this can always be done:
SELECT (SELECT COUNT(*)
FROM product_a
WHERE product_id IN (SELECT Products
FROM #WorkTable)
) = (SELECT COUNT(*)
FROM #WorkTable)
If the IN clause is complex then either spool it into a table or write it twice.
If you have the IDs stored in a temp table (which can be done by some C# function or simple SQL) then the problem becomes easy and doable in SQL.
select "all exist"
where (select case when count(distinct t.id) = (select count(distinct id) from #products) then "true" else "false" end
from ProductTable t, #products p
where t.id = p.id) = "true"
This will return "all exists" when all the products in #products exist in the target table (ProductTable) and will not return a row if the above is not true.
If you are not willing to write to a temp table, then you need to feed in some parameter for the number of products you are attempting to find, and replace the temp table with an 'in'; clause so the subquery looks like this:
SELECT "All Exist"
WHERE(
SELECT case when count(distinct t.id) = #ProductCount then "true" else "false"
FROM ProductTable t
WHERE t.id in (1,100,10,20) -- example IDs
) = "true"
If you are using SQL Server 2008, I would create a stored procedure which takes a table-valued parameter. The query should then be of a particularly simple form:
CREATE PROCEDURE usp_CheckAll
(#param dbo.ProductTableType READONLY)
AS
BEGIN
SELECT CAST(1 AS bit) AS Result
WHERE (SELECT COUNT(DISTINCT ProductID) FROM #param)
= (SELECT COUNT(DISTINCT p.ProductID) FROM #param AS p
INNER JOIN Products
ON p.ProductID = Products.ProductID)
END
I changed this to return a row, as you seem to require. There are other ways to do this with a WHERE NOT EXISTS (LEFT JOIN in here WHERE rhs IS NULL):
CREATE PROCEDURE usp_CheckAll
(#param dbo.ProductTableType READONLY)
AS
BEGIN
SELECT CAST(1 AS bit) AS Result
WHERE NOT EXISTS (
SELECT * FROM #param AS p
LEFT JOIN Products
ON p.ProductID = Products.ProductID
WHERE Products.ProductID IS NULL
)
END
Your c# will have to do just a bit of work (counting the number of IDs passed in), but try this:
select (select count(*) from players where productid in (1, 10, 100, 1000)) = 4
Edit:
4 can definitely be parameterized, as can the list of integers.
If you're not generating the SQL from string input by the user, you don't need to worry about attacks. If you are, you just have to make sure you only get integers. For example, if you were taking in the string "1, 2, 3, 4", you'd do something like
String.Join(",", input.Split(",").Select(s => Int32.Parse(s).ToString()))
That will throw if you get the wrong thing. Then just set that as a parameter.
Also, be sure be sure to special case if items.Count == 0, since your DB will choke if you send it where ParameterID in ().
Where is this list of products that you're trying to determine the existence of? If that list exists within another table you could do this
declare #are_equal bit
declare #products int
SELECT #products =
count(pl.id)
FROM ProductList pl
JOIN Products p
ON pl.productId = p.productId
select #are_equal = #products == select count(id) from ProductList
Edit:
Then do ALL the work in C#. Cache the actual list of products in your application somewhere, and do a LINQ query.
var compareProducts = new List<Product>(){p1,p2,p3,p4,p5};
var found = From p in GetAllProducts()
Join cp in compareProducts on cp.Id equals p.Id
select p;
return compareProducts.Count == found.Count;
This prevents constructing SQL queries by hand, and keeps all your application logic in the application.
This may be too simple, but I always use:
SELECT COUNT(*)>0 FROM `table` WHERE condition;
Example:
SELECT iif(count(id)=0,'false','true') FROM table WHERE id = 19