SQL Many to Many relationship - sql

I'm having difficulties writing a SQL query. This is the structure of 3 tables, table Race_ClassificationType is many-to-many table.
Table Race
----------------------------
RaceID
Name
Table Race_ClassificationType
----------------------------
Race_ClassificationTypeID
RaceID
RaceClassificationID
Table RaceClassificationType
----------------------------
RaceClassificationTypeID
Name
What I'm trying to do is get the races with certain classifications. The results are returned by a store procedure that has a table-value parameter which holds the desired classifications:
CREATE TYPE [dbo].[RaceClassificationTypeTable]
AS TABLE
(
RaceClassificationTypeID INT NULL
);
GO
CREATE PROCEDURE USP_GetRaceList
(#RaceClassificationTypeTable AS [RaceClassificationTypeTable] READONLY,
#RaceTypeID INT = NULL,
#IsCompleted BIT = NULL,
#MinDateTime DATETIME = NULL,
#MaxDateTime DATETIME = NULL,
#MaxRaces INT = NULL)
WITH RECOMPILE
AS
BEGIN
SET NOCOUNT ON;
SELECT DISTINCT
R.[RaceID]
,R.[RaceTypeID]
,R.[Name]
,R.[Abbreviation]
,R.[DateTime]
,R.[IsCompleted]
FROM [Race] R,[Race_ClassificationType] R_CT, [RaceClassificationType] RCT
WHERE (R.[RaceTypeID] = #RaceTypeID OR #RaceTypeID IS NULL)
AND (R.[IsCompleted] = #IsCompleted OR #IsCompleted IS NULL)
AND (R.[DateTime] >= #MinDateTime OR #MinDateTime IS NULL)
AND (R.[DateTime] <= #MaxDateTime OR #MaxDateTime IS NULL)
AND (R.RaceID = R_CT.RaceID)
AND (R_CT.RaceClassificationTypeID = RCT.RaceClassificationTypeID)
AND (RCT.RaceClassificationTypeID IN (SELECT DISTINCT T.RaceClassificationTypeID FROM #RaceClassificationTypeTable T))
ORDER BY [DateTime] DESC
OFFSET 0 ROWS FETCH NEXT #MaxRaces ROWS ONLY
END
GO
As it is this stored procedure doesnt work correctly because it returns all races that have at least one classification type ID in the table-value parameter of classification type IDs (because of the IN clause). I want that the store procedure returns only races that have all the classifications supplied in the table-valued parameter.
Example:
RaceClassificationTypeID RaceID
3 92728
3 92729
8 92729
29 92729
12 92729
2 92729
3 92730
8 92730
8 92731
1 92731
RaceClassificationTypeIDs in RaceClassificationTypeTable parameter: 3 and 8
OUTPUT: all the races with RaceClassificationID 3 and 8 and optionally any other (2, 29, 12)
That means only races 92729 and 92730 should be returned, as it is all the races in the example are returned.

I've set up two tables, one stores your result set and the other represents the values in the table valued parameter of your stored procedure. See below.
CREATE TABLE ABC
(
RCTID INT,
RID INT
)
INSERT INTO ABC VALUES (3,92728)
INSERT INTO ABC VALUES (3,92729)
INSERT INTO ABC VALUES (8,92729)
INSERT INTO ABC VALUES (29,92729)
INSERT INTO ABC VALUES (12,92729)
INSERT INTO ABC VALUES (2,92729)
INSERT INTO ABC VALUES (3,92730)
INSERT INTO ABC VALUES (8,92730)
INSERT INTO ABC VALUES (8,92731)
INSERT INTO ABC VALUES (1,92731)
GO
CREATE TABLE TABLEVALUEPARAMETER
(
VID INT
)
INSERT INTO TABLEVALUEPARAMETER VALUES (3)
INSERT INTO TABLEVALUEPARAMETER VALUES (8)
GO
SELECT RID FROM ABC WHERE RCTID IN (SELECT VID FROM TABLEVALUEPARAMETER) GROUP BY
RID HAVING COUNT(RID) = (SELECT COUNT(VID) FROM TABLEVALUEPARAMETER)
GO
If you run this on your machine you'll notice it produces the two IDs that you're after.
Because you have a stored procedure with a lot of columns selected it would be necessary to use a CTE (Common Table Expression). This is because if you were to try to group all the columns in the current select statement you would have to group by all the columns and you would then get duplication.
If the first CTE delivers the result set and then you uses a version of the select above you should be able to produce only the IDs you want.
If you don't know CTE's let me know!

This is an example of a "set-within-sets" subquery. One way to solve this is with aggregation and a having clause. Here is how you get the RaceIds:
select RaceID
from RaceClassification rc
group by RaceID
having sum(case when RaceClassificationTypeId = 3 then 1 else 0 end) > 0 and
sum(case when RaceClassificationTypeId = 8 then 1 else 0 end) > 0;
Each condition in the having clause is counts how many rows have each type. Only races with each (because of the > 0) are kept.
You can get all the race information by using this as a subquery:
select r.*
from Races r join
(select RaceID
from RaceClassification rc
group by RaceID
having sum(case when RaceClassificationTypeId = 3 then 1 else 0 end) > 0 and
sum(case when RaceClassificationTypeId = 8 then 1 else 0 end) > 0
) rc
on r.RaceID = rc.RaceId;
Your stored procedure seems to have other conditions. These can also be added in.

Related

SQL UPDATE query - value depends on another rows

There is a SQL Server database temporary table, let it be TableA. And the table structure is following:
CREATE TABLE #TableA
(
ID BIGINT IDENTITY (1, 1) PRIMARY KEY,
MapVal1 BIGINT NOT NULL,
MapVal2 BIGINT NOT NULL,
IsActual BIT NULL
)
The table is already filled with some mappings of MapVal1 to MapVal2. The issue is that not all the mappings should be flagged as Actual. For this reason should be used IsActual column. Currently IsActual is set to NULL for every row. The task is to create the query for updating IsActual column value. UPDATE query should follow next conditions:
If MapVal1 is unique and MapVal2 is unique (one-to-one mapping) - then this mapping should be flagged as Actual, so IsActual = 1;
If MapVal1 is not unique - then Actual should be the mapping of current MapVal1 to smallest MapVal2, and this MapVal2 must be not mapped to any other MapVal1 that is smaller than current MapVal1;
If MapVal2 is not unique - then Actual should be the mapping of current MapVal2 to smallest MapVal1, and this MapVal1 must be not mapped to any other MapVal2 that is smaller than current MapVal2;
All rows that are not fulfill any of 1), 2) or 3) conditions - should be flagged as inactual, so IsActual = 0.
I believe there is relation between Condition 2) and Condition 3). For every row they both are fulfilled or both are not.
To make it clear, here is an example of result I want to obtain:
Result should be that every MapVal1 is mapped to just one MapVal2 and vice varsa every MapVal2 is mapped to just one MapVal1.
I have created sql-query to resolve my task:
IF OBJECT_ID('tempdb..#TableA') IS NOT NULL
BEGIN
DROP TABLE #TableA
END
CREATE TABLE #TableA
(
ID BIGINT IDENTITY (1, 1) PRIMARY KEY,
MapVal1 BIGINT NOT NULL,
MapVal2 BIGINT NOT NULL,
IsActual BIT NULL
)
-- insert input data
INSERT INTO #TableA (MapVal1, MapVal2)
SELECT 1, 1
UNION ALL SELECT 1, 3
UNION ALL SELECT 1, 4
UNION ALL SELECT 2, 1
UNION ALL SELECT 2, 3
UNION ALL SELECT 2, 4
UNION ALL SELECT 3, 3
UNION ALL SELECT 3, 4
UNION ALL SELECT 4, 3
UNION ALL SELECT 4, 4
UNION ALL SELECT 6, 7
UNION ALL SELECT 7, 8
UNION ALL SELECT 7, 9
UNION ALL SELECT 8, 8
UNION ALL SELECT 8, 9
UNION ALL SELECT 9, 8
UNION ALL SELECT 9, 9
CREATE NONCLUSTERED INDEX IX_Mapping_MapVal1 ON #TableA (MapVal1);
CREATE NONCLUSTERED INDEX IX_Mapping_MapVal2 ON #TableA (MapVal2);
-- UPDATE of #TableA is starting here
-- every one-to-one mapping should be actual
UPDATE m1 SET
m1.IsActual = 1
FROM #TableA m1
LEFT JOIN #TableA m2
ON m1.MapVal1 = m2.MapVal1 AND m1.ID <> m2.ID
LEFT JOIN #TableA m3
ON m1.MapVal2 = m3.MapVal2 AND m1.ID <> m3.ID
WHERE m2.ID IS NULL AND m3.ID IS NULL
-- update for every one-to-many or many-to-many mapping is more complicated
-- would be great to change this part of query to make it witout any LOOP
DECLARE #MapVal1 BIGINT
DECLARE #MapVal2 BIGINT
DECLARE #i BIGINT
DECLARE #iMax BIGINT
DECLARE #LoopCount INT = 0
SELECT
#iMax = MAX (m.ID)
FROM #TableA m
SELECT
#i = MIN (m.ID)
FROM #TableA m
WHERE m.IsActual IS NULL
WHILE #i <= #iMax
BEGIN
SELECT #LoopCount = #LoopCount + 1
SELECT
#MapVal1 = m.MapVal1,
#MapVal2 = m.MapVal2
FROM #TableA m
WHERE m.ID = #i
IF EXISTS
(
SELECT *
FROM #TableA m
WHERE
m.ID < #i
AND
(m.MapVal1 = #MapVal1
OR m.MapVal2 = #MapVal2)
AND m.IsActual IS NULL
)
BEGIN
UPDATE m SET
m.IsActual = 0
FROM #TableA m
WHERE m.ID = #i
END
SELECT #i = MIN (m.ID)
FROM #TableA m
WHERE
m.ID > #i
AND m.IsActual IS NULL
END
UPDATE m SET
m.IsActual = 1
FROM #TableA m
WHERE m.IsActual IS NULL
SELECT * FROM #TableA
but as it was expected performance of the query with LOOP is very bad, specially when input table keep millions of rows. I spent a lot of time trying to produce query without LOOP to get reduce execution time of my query but unsuccessfully.
Could anybody advice me how to improve performance of my query. It would be great to get query without LOOP.
Using a loop does not imply you need to update the table one record at a time.
It may help if each individual UPDATE statement updates multiple records.
Consider all possible combinations of MapVal1 and MapVal2 as a matrix.
Every time you flag a cell as 'actual', you can flag an entire row and an entire column as 'not actual'.
The simplest way to do this, is by following these steps.
Of all mappings with IsActual = NULL, take the first one (smallest MapVal1, together with the smallest MapVal2 it is mapped to).
Flag this mapping as actual (IsActual = 1).
Flag all other mappings with the same MapVal1 as non-actual (IsActual = 0).
Flag all other mappings with the same MapVal2 as non-actual (IsActual = 0).
Repeat from step 1 until no more records with IsActual = NULL exist.
Here's an implementation:
SELECT 0 -- force ##ROWCOUNT initially 1
WHILE ##ROWCOUNT > 0
WITH MakeActual AS (
SELECT TOP 1 MapVal1, MapVal2
FROM #TableA
WHERE IsActual IS NULL
ORDER BY MapVal1, MapVal2
)
UPDATE a
SET IsActual = CASE WHEN a.MapVal1 = m.MapVal1 AND a.MapVal2 = m.MapVal2 THEN 1 ELSE 0 END
FROM #TableA a
INNER JOIN MakeActual m ON a.MapVal1 = m.MapVal1 OR a.MapVal2 = m.MapVal2
The number of loop iterations equals the number of 'actual' mappings.
The actual performance gain depends a lot on the data.
If the majority of mappings is one-to-one (i.e. hardly any non-actual mappings), then my algorithm will make little difference.
Therefore, it may be wise to keep the initial UPDATE statement from your own code sample (the one with the comment "every one-to-one mapping should be actual").
It may also help to play around with the indexes.
This one should help to further optimize the clause ORDER BY MapVal1, MapVal2:
CREATE NONCLUSTERED INDEX IX_MapVals ON #TableA (MapVal1, MapVal2)

ignoring last value if its the same as current value

I have an table where i would like to query the following:
The data comes in batches . This data is combined with an id.
This ID only gets send ones when the new batch comes in. After that the ID only changes when there is a new batch . In the mean time the value stays null
What i need to do is if new data comes in and it has the same id as the previous batch i have to continue the insert with null in the id field instead of pushing a new row with the same id value.
Beneath is a simplistic view of the table
ID Values
1 10
null 20
null 20
null 20
null 20
2 20
null 20
null 20
null 20
null 20
1 20
null 20
If you could help me point in a directions that would help me a lot.
Maybe to clearify the id value is a set of tags. So there are some definied tags(100 or more) and when a new batch comes the batch gets a tag with it. And if that tag is the same as the previous the null has to continue instead of inserting the same tag
You'll need to add an identity field (or a timestamp) in order to be able to query the latest ID.
ALTER TABLE MyTable ADD MyIdent INT IDENTITY(1, 1) NOT NULL
Then on your insert (if your Id value is NULL) you can call
INSERT INTO MyTable (Id, Values)
SELECT TOP 1 Id, #ValuesVariable
FROM MyTable
WHERE Id IS NOT NULL
ORDER BY MyIdent DESC
This below Sp may helps to inert data try this
IF OBJECT_ID('tempdb..#Temp')IS NOT NULL
DROP TABLE #Temp
CREATE TABLE #Temp (ID INT,[Values] INT)
CREATE PROCEDURE usp_Insert
(
#Id INT,
#Values INT
)
AS
BEGIN
IF NOT EXISTS (SELECT 1 FROM #Temp WHERE ID = #ID)
BEGIN
INSERT INTO #Temp(ID,[Values])
SELECT #Id,#Values
END
ELSE
INSERT INTO #Temp(ID,[Values])
SELECT NULL,#Values
END
EXEC usp_Insert 2,12
SELECT * FROM #Temp

Know identity before insert

I want to copy rows from the table within the table itself. But before inserting I need to modify a varchar column appending the value of identity column to it.
My table structure is:
secID docID secName secType secBor
1 5 sec-1 G 9
2 5 sec-2 H 12
3 5 sec-3 G 12
4 7 sec-4 G 12
5 7 sec-5 H 9
If I want to copy data of say docID 5, currently this runs through a loop one row at a time.
I can write my query as
insert into tableA (docID, secName, secType, secBor)
select 8, secName, secType, secBor from tableA where docID = 5
But how can I set value of secName before hand so that it becomes sec-<value of secID column>?
Don't try to guess the value of identity column. In your case you could simply create a computed column secName AS CONCAT('sec-', secID). There is no further need to update that column.
DB Fiddle
It is also possible to create an AFTER INSERT trigger to update the column.
Since SQL Server does not have GENERATED ALWAYS AS ('Sec - ' + id) the only simple option I see is to use a trigger.
Adding to my comment something like:
insert into tableA (docID, secName, secType, secBor)
select
ROW_NUMBER() OVER (ORDER BY DocID),
'Sec -'+ ROW_NUMBER() OVER (ORDER BY DocID),
secType, secBor
from tableA
where docID = 5
In SQL Server 2012 and later, you can achieve this by using the new sequence object.
CREATE SEQUENCE TableAIdentitySeqeunce
START WITH 1
INCREMENT BY 1 ;
GO
create table TableA
(
secId int default (NEXT VALUE FOR TableAIdentitySeqeunce) not null primary key,
varcharCol nvarchar(50)
)
declare #nextId int;
select #nextId = NEXT VALUE FOR TableAIdentitySeqeunce
insert TableA (secId, varcharCol)
values (#nextId, N'Data #' + cast(#nextId as nvarchar(50)))

In a persisted field, how do you return the number of occurrences of a column within a different table's column

The following is required due to records being entered by 3rd parties in a web application.
Certain columns (such as Category) require validation including the one below. I have a table OtherTable with the allowed values.
I need to identify how many occurrences (ie: IF) there are of the current table's column's value in a different table's specified column. If there are no occurrences this results in a flagged error '1', if there are occurrences, then it results in no flagged error '0'.
If `Category` can be found in `OtherTable.ColumnA` then return 0 else 1
How can I do this please?
If Category can be found in OtherTable.ColumnA then return 0 else 1
You could use CASE with EXISTS
SELECT CASE WHEN EXISTS(
SELECT NULL
FROM AllowedValues av
WHERE av.ColumnA = Category
) THEN 0 ELSE 1 END AS ErrorCode
, Category
FROM [Table]
Edit: Here's a sql-fiddle: http://sqlfiddle.com/#!3/55a2e/1
Edit: I've only just noticed that you want to use a computed column. As i've read you can only use it with scalar values and not with sub-queries. But you can create a scalar valued function.
For example:
create table AllowedValues(ColumnA varchar(1));
insert into AllowedValues Values('A');
insert into AllowedValues Values('B');
insert into AllowedValues Values('C');
create table [Table](Category varchar(1));
insert into [Table] Values('A');
insert into [Table] Values('B');
insert into [Table] Values('C');
insert into [Table] Values('D');
insert into [Table] Values('E');
-- create a scalar valued function to return your error-code
CREATE FUNCTION udf_Category_ErrorCode
(
#category VARCHAR(1)
)
RETURNS INT
AS BEGIN
DECLARE #retValue INT
SELECT #retValue =
CASE WHEN EXISTS(
SELECT NULL
FROM AllowedValues av
WHERE av.ColumnA = #category
) THEN 0 ELSE 1 END
RETURN #retValue
END
GO
Now you can add the column as computed column which uses the function to calculate the value:
ALTER TABLE [Table] ADD ErrorCode AS ( dbo.udf_Category_ErrorCode(Category) )
GO
Here's the running SQL: http://sqlfiddle.com/#!3/fc49e/2
Note: as #Damien_The_Unbelieve has commented at the other answer, even if you persist the result with a UDF, the value won't be updated if the rows in OtherTable change. Just keep that in mind, so you need to update the table manually if desired with the help of the UDF.
select mt.*,IFNULL(cat_count.ct,0) as Occurrences from MainTable mt
left outer join (select ColumnA,count(*) as ct from OtherTable) cat_count
on mt.Category=cat_count.ColumnA
Result:
mt.col1 | mt.col2 | Category | Occurrences
### | ### | XXX | 3
### | ### | YYY | 0
### | ### | ZZZ | 1

Nightmare Case Statement Assistance pl

Table 1 - Deal ID, REF NOS, Type, Papa ID
Table 2 - Deal ID, Type
Making a column in a new view called Method used. The way the field is to be set is as follows ( 4 conditions);
If Deal ID from table 1 Exists in Table 2 and Type is not Null from Table 2.
Set Method used to be Y
If Deal ID does not exist in Table 1 and Type does not contain 27,42 or 55 in Table 1.
Set Method used to be Y
If Papa ID is null from Table 1, and Type does not contain 27,42 or 55 in Table 1.
Set Method used to be Y
Else
Set to N
Started it and thought wow!..
create view Master as (
select Deal ID, REF NOS, Type, Papa ID
[Method used]=
Case
When
from Table 1 A
)
Something like this may work (assuming that these tables join on DealId). Note, I've removed spaces from some of your column names that you showed in your question.
Given these tables:
CREATE TABLE #Table1 (DealId INT, RefNos VARCHAR(100), [Type] VARCHAR(100), PapaId INT);
CREATE TABLE #Table2 (DealId INT, [Type] VARCHAR(100));
view example:
WITH DealIds AS (
SELECT DealId FROM #Table1
UNION
SELECT DealId FROM #Table2
)
SELECT
CASE
-- If Deal ID from table 1 Exists in Table 2 and Type is not Null from Table 2.
-- Set Method used to be Y
WHEN t1.DealId IS NOT NULL AND t2.DealId IS NOT NULL AND t2.[Type] IS NOT NULL THEN 'Y'
-- If Deal ID does not exist in Table 1 and Type does not contain 27,42 or 55 in Table 1.
-- Set Method used to be Y
-- Note: it is is redundant to have the type condition if DealId is the PK.
WHEN t1.DealId IS NULL AND t1.[Type] NOT IN (27, 42, 55) THEN 'Y'
-- If Papa ID is null from Table 1, and Type does not contain 27,42 or 55 in Table 1.
-- Set Method used to be Y
WHEN t1.PapaId IS NULL AND t1.[Type] NOT IN (27,42, 55) THEN 'Y'
-- Else
-- Set to N
ELSE 'N'
END AS MethodUsed
FROM DealIds d
LEFT JOIN #Table1 t1 ON d.DealId = t1.DealId
LEFT JOIN #Table2 t2 ON d.DealId = t2.DealId