I was wondering if it is possible to retrieve the net changes similar to cdc.fn_cdc_get_net_changes_<capture_instance>(from_lsn , to_lsn, 'all with mask') of tables that don't have a primary key but do have a constraint that ensures that one (or more) column(s) is unique.
It took me a while but I think I have a solution that works, let me know if there's a better solution or if you see a bug in mine.
Let's assume a capture instance named capture_instance of a table with unique column ID and non-unique columns field1, field2 and field3 and variables #from_lsn and #to_lsn.
WITH
cdc_all AS (
-- Retrieve the change table with all changes
SELECT *
FROM cdc.fn_cdc_get_all_changes_capture_instance(#from_lsn, #to_lsn, 'all')
),
f AS (
SELECT cdc_all.*, ops.[delete], ops.[insert], ops.[update], ops.[net_op]
FROM cdc_all
INNER JOIN (
-- Retrieve three flags for insert, update and delete and the net operation
-- also filter insert + delete pairs because it results in no change
SELECT *
FROM (
SELECT ID
, MAX(CASE WHEN __$operation = 1 THEN 1 ELSE 0 END) as [delete]
, MAX(CASE WHEN __$operation = 2 THEN 1 ELSE 0 END) as [insert]
, MAX(CASE WHEN __$operation = 4 THEN 1 ELSE 0 END) as [update]
, MIN(__$operation) [net_op]
FROM cdc_all
GROUP BY ID
) ops
WHERE NOT (ops.[delete] = 1 AND ops.[insert] = 1)
) ops ON cdc_all.ID = ops.ID
)
SELECT net.[max_lsn], f.[net_op] __$operation
, (CASE WHEN net.__$update_mask != 0x0 THEN net.__$update_mask ELSE NULL END) __$update_mask
, f.[ID], [field1], [field2], [field3]
FROM f
INNER JOIN (
-- bitwise OR the __$update_mask of the updates
-- also retrieve the last lsn of each row which should be used as the __$start_lsn of the result set
SELECT ID
, CAST(SUM(DISTINCT CAST((CASE WHEN f.[__$operation] = 4 AND f.[insert] != 1 THEN f.[__$update_mask] ELSE 0 END) as int)) as varbinary(2)) [__$update_mask]
, MAX(__$start_lsn) [max_lsn]
FROM f
GROUP BY ID
) net ON f.ID = net.ID AND f.__$start_lsn = net.[max_lsn]
To match the behavior of cdc.fn_cdc_get_net_changes_ exactly the size of the varbinary at the end should be as small as possible for all fields to fit, but a larger value wouldn't break the functionality.
Related
While using case when in where clause in sql query it's not working.
Problem :
I have two tables named TblEmployee and TblAssociate.Both tables contains common columns PeriodId, EmpId and AssociateId. My requirement is to fetch values from
TblEmployee with combination of EmpId and AssociateId from TblAssociate should be excluded.And the exclusion should be based on PeriodId condition.`
If(#PeriodID<50)
BEGIN
SELECT *
FROM TblEmployee
WHERE (EmpId+AssociateId) NOT IN (SELECT EmpId+AssociateId FROM TblAssociate)
END
ELSE
BEGIN
SELECT *
FROM TblEmployee
WHERE (EmpId) NOT IN (SELECT EmpId FROM TblAssociate)
END
The above code is working, but I need to avoid that IF-ELSE condition and I wish to use 'case when' in where clause.Please help
Try this:
SELECT *
FROM TblEmployee
WHERE (EmpId + CASE WHEN #PeriodID<50 THEN AssociateId ELSE 0 END) NOT IN
(SELECT EmpId + CASE WHEN #PeriodID<50 THEN AssociateId ELSE 0 END FROM TblAssociate)
You say your code is working but this is rather odd, since it doesn't make much sense to add together id values. In any case, the above statement produces a result that is equivalent to the code originally posted.
You could use AND-OR combination in the WHERE clause. Additionally, you should not be using + as it may lead to incorrect result. You can rewrite your query as:
SELECT e.*
FROM TblEmployee e
WHERE
(
#PeriodID < 50
AND NOT EXISTS(
SELECT 1
FROM TblAssociate a
WHERE
a.EmpId = e.EmpId
AND a.AssociateId = e.AssociateId
)
)
OR
(
#PeriodID >= 50
AND NOT EXISTS(
SELECT 1
FROM TblAssociate a
WHERE a.EmpId = e.EmpId
)
)
The addition of IDs do not guarantee uniqueness. For instance, if EmpId is 5 and AssociateId is 6, then EmpId + AssociateId = 11, while EmpId + AssociateId = 11 even if EmpId is 6 and AssociateId is 5. In the query below, I made sure that the subquery will stop searching when the first record is found and will return a single record, having the value of 1. We select the employee if and only if 1 is among the results. In the subquery we check the operand we are sure of first and then check if we are not in a period where AssociateId must be checked, or it matches.
select *
from TblEmployee
where 1 in (select top 1 1
from TblAssociate
where TblEmployee.EmpId = TblAssociate.EmpId and
(#PeriodID >= 50 or TblEmployee.AssociateId = TblAssociate.AssociateId))
I'm fairly new to SQL. This site has been a great resource and I found answers to many questions so far. Now I'm a little stuck, so it's time for my first question.
I'm working in SQL Server 2012. How do I update FLAG = 'Y' when START_DATE = (SELECT CONTRACT, MIN(START_DATE) FROM #CONTR GROUP BY CONTRACT) ?
CREATE TABLE #CONTR
(
CONTRACT INT , -- PRIMARY KEY COL1
CONTRACT_LINE INT , -- PRIMARY KEY COL2
START_DATE INT , -- 0 = CURRENT MONTH, -3 = THREE MONTHS IN PAST
FLAG VARCHAR(1) -- 'Y' OR 'N', DEFAULTED TO 'N' WHEN TABLE POPULATED
)
This doesn't seem like it should be that difficult, but I just can't seem to get it to work.
UPDATE #CONTR
SET FLAG = 'Y'
FROM #CONTR C1
JOIN
(SELECT CONTRACT, MIN(START_DATE) AS START_DATE FROM #CONTR GROUP BY CONTRACT) C2
ON C1.START_DATE = C2.START_DATE
INSERT INTO #CONTR VALUES (1,1,100,'N')
INSERT INTO #CONTR VALUES (1,1,200,'N')
INSERT INTO #CONTR VALUES (2,1,100,'N')
INSERT INTO #CONTR VALUES (2,1,200,'N')
UPDATE
c
SET
c.FLAG = 'Y'
FROM
#CONTR c
JOIN (
SELECT
CONTRACT _CONTRACT ,
MAX(START_DATE) _START_DATE
FROM
#CONTR
GROUP BY
CONTRACT
) mc ON c.CONTRACT = mc.[_CONTRACT] AND c.START_DATE = mc.[_START_DATE]
Another way to do that. It is absolutely the same, just the syntax is different:
UPDATE
c
SET
c.FLAG = 'Y'
FROM
#CONTR c
CROSS APPLY (
SELECT
c1.CONTRACT _CONTRACT
FROM
#CONTR c1
WHERE c1.CONTRACT = c.CONTRACT
GROUP BY
CONTRACT
HAVING MAX(c1.START_DATE) = c.START_DATE
) mc;
And the results:
SELECT * FROM #CONTR
CONTRACT CONTRACT_LINE START_DATE FLAG
1 1 100 N
1 1 200 Y
2 1 100 N
2 1 200 Y
Post has been revised!!!....
I need help on the WHERE condition as the following:
ID# 1 belongs to Engineering Department.
ID# 2 belongs to Sales Department
ID# 3 belongs to Other Department.
ID# 4 belongs to Eng-Level2 department
ID# 5 belongs to Eng-Level3 department
ID# 6 belongs to Eng-Level4 department
What I try to accomplish is, if ID# 1 log run this report, it will show the activities belong to him (ID# 1), AND Eng-Levelx (ID#4, ID#5, and ID#6). However, if ID belongs to another Departments (ID# 2 & 3), will ONLY show the activities below to his/her.
Here is my non working query:
SELECT * FROM dbo.CRM_Activity
WHERE
(ActivityOwnerID = 1579
AND [Status] = 'Open'
AND Status <> 'Deleted')
OR
ActivityOwnerID IN (
SELECT COALESCE(
(SELECT Tech_ID FROM dbo.employee WHERE tech_ID = 1579 and POSITION = 'Engineering')
, (SELECT Tech_ID FROM dbo.employee WHERE LEFT(first_name, 4) = 'Eng-') --- THIS ONE FAILED BECAUSE IT RETURNS MULTIPLE RECORDS
)
)
AND [Status] = 'Open'
ORDER BY CreatedDate DESC
You could just use UNION instead, so this will get a list of unique Tech_ID's from both queries.
SELECT * FROM dbo.CRM_Activity
WHERE
ActivityOwnerID IN (
SELECT Tech_ID FROM dbo.employee WHERE tech_ID = 1579 and POSITION = 'Engineering'
UNION
SELECT Tech_ID FROM dbo.employee WHERE LEFT(first_name, 4) = 'Eng-'
)
In your case SELECT COALESCE(.... can return more than one value because IN allows multiple values, but result of SELECT Tech_ID FROM dbo.employee WHERE tech_ID = 1579 and POSITION = 'Engineering' or so must be one because COALESCE expects expression (not result set).
I have the following table:
Items:
ID Type StockExists
01 Cellphone T
02 Cellphone F
03 Apparrel T
I want to count the number of items with existing stocks, i.e., the number of rows with StockExists='T'. I was performing the query as;
Select count(StockExists)
From [Items] where StockExists='T'
but it is always returning 1. What is the right way to do it?
Edit:
Also, how to perform another such Count operation and add them together in one row, for example,
Select count(StockExists)
From [Items] where StockExists='T'` and `Select count(Type)
From [Items] where Type='Cellphone'` ?
SELECT
COUNT(*) As ExistCount
FROM
dbo.Items
WHERE
StockExists='T'
So your query should work.
Result:
EXISTCOUNT
2
Demo
Update
How to perform another such Count operation and add them together in
one row, for example, Select count(StockExists) From [Items] where
StockExists='T' and Select count(Type) From [Items] where
Type='Cellphone' ?
You can use SUM with CASE:
SELECT
ExistCount = SUM(CASE WHEN StockExists='T' THEN 1 ELSE 0 END) ,
CellphoneCount = SUM(CASE WHEN Type='Cellphone' THEN 1 ELSE 0 END)
FROM
dbo.Items
Result:
EXISTCOUNT CELLPHONECOUNT
2 2
Demo
Select Sum(Case when field = 'this' then 1 else 0 end) as Total
from YourTable
When using CASE WHEN better to use NULL than 0 in ELSE case like below
SELECT
ExistCount = SUM(CASE WHEN StockExists='T' THEN 1 ELSE NULL END) ,
TotalCount = COUNT(ID)
FROM
dbo.Items
I want to create a sql query to split a single column value into multiple rows like:
SELECT ID, PRODUCT_COUNT FROM MERCHANT WHERE ID = 3050
ID PRODUCT_COUNT
----------- -------------
3050 591
Based on this result, I want 6 rows as follows:
ID RANGE
3050 0-100
3050 101-200
3050 201-300
3050 301-400
3050 401-500
3050 501-591
How can I acheive this in a query ?
WITH cte AS (
SELECT
m.ID,
PRODUCT_COUNT,
LoBound = (v.number - 1) * 100 + 1,
HiBound = v.number * 100
FROM MERCHANT m
INNER JOIN master..spt_values v
ON v.type = 'P' AND v.number BETWEEN 1 AND (m.PRODUCT_COUNT - 1) / 100 + 1
WHERE m.ID = 3050
)
SELECT
ID,
RANGE = CAST(CASE LoBound
WHEN 1 THEN 0
ELSE LoBound
END AS varchar)
+ '-'
+ CAST(CASE
WHEN HiBound < PRODUCT_COUNT THEN HiBound
ELSE PRODUCT_COUNT
END AS varchar)
FROM cte
The first CASE makes sure the first range starts with 0, not with 1, same as in your sample output.
Sorry... code removed. I made a mistake where if the Product_Count was evenly divisible by 100, it gave an incorrect final row.
UPDATE:
Andriy's code is still correct. I was missing a "-1" in mine. I've repaired that and reposted both the test setup and my alternative solution.
Both Andriy's and my code produce the output in the correct order for this experiment, but I added an ORDER BY to guarantee it.
Here's the code for the test setup...
--===== Conditionally drop and create a test table for
-- everyone to work against.
IF OBJECT_ID('tempdb..#Merchant','U') IS NOT NULL
DROP TABLE #Merchant
;
SELECT TOP 10000
ID = IDENTITY(INT,1,1),
Product_Count = ABS(CHECKSUM(NEWID()))%100000
INTO #Merchant
FROM sys.all_columns ac1
CROSS JOIN sys.all_columns ac2
;
ALTER TABLE #Merchant
ADD PRIMARY KEY CLUSTERED (ID)
;
--===== Make several entries where there's a known test setup.
UPDATE #Merchant
SET Product_Count = CASE
WHEN ID = 1 THEN 0
WHEN ID = 2 THEN 1
WHEN ID = 3 THEN 99
WHEN ID = 4 THEN 100
WHEN ID = 5 THEN 101
WHEN ID = 6 THEN 99999
WHEN ID = 7 THEN 100000
WHEN ID = 8 THEN 100001
END
WHERE ID < = 8
;
Here's the alternative I posted before with the -1 correction.
WITH
cteCreateRanges AS
(--==== This determines what the ranges are
SELECT m.ID,
RangeStart = t.Number*100+SIGN(t.Number),
RangeEnd =(t.Number+1)*100,
Product_Count
FROM master.dbo.spt_Values t
CROSS JOIN #Merchant m
WHERE t.Number BETWEEN 0 AND (m.Product_Count-1)/100
AND t.Type = 'P'
AND m.ID BETWEEN 1 AND 8 -- = #FindID -<<<---<<< Or use a single variable to find.
)--==== This makes the output "pretty" and sorts in correct order
SELECT ID,
[Range] = CAST(RangeStart AS VARCHAR(10)) + '-'
+ CASE
WHEN RangeEnd <= Product_Count
THEN CAST(RangeEnd AS VARCHAR(10))
ELSE CAST(Product_Count AS VARCHAR(10))
END
FROM cteCreateRanges
ORDER BY ID, RangeStart
;
Sorry about the earlier mistake. Thanks, Andriy, for catching it.
You could create a table like this (I am changing the first range to include 100 elements like the others to make it easier, and basing it at one, so that the indexes will match the total count):
CountRangeBoundary
MinIndexInRange
---------------
1
101
201
301
401
501
601
...
Then do a θ-join like this:
SELECT m.ID,
crb.MinIndexInRange AS RANGE_MIN,
MIN( crb.MinIndexInRange + 100, m.PRODUCT_COUNT) AS RANGE_MAX
FROM MERCHANT m
JOIN CountRangeBoundry crb ON crb.MinIndexInRange <= m.PRODUCT_COUNT
WHERE m.ID = 3050
It looks like those ranges are a piece of data, so they should really be in a table (even if you don't expect them to change, because they will). That has the nice side benefit of making this task trivial:
CREATE TABLE My_Ranges ( -- Use a more descriptive name
range_start SMALLINT NOT NULL,
range_end SMALLINT NOT NULL,
CONSTRAINT PK_My_Ranges PRIMARY KEY CLUSTERED (range_start)
)
GO
SELECT
P.id,
R.range_start,
CASE
WHEN R.range_end < P.product_count THEN R.range_end
ELSE P.product_count
END AS range_end
FROM
Products P
INNER JOIN My_Ranges R ON
R.range_start <= P.product_count
If your ranges will always be contiguous then you can omit the range_end column. Your query will become a little more complex, but you won't have to worry about ranges overlapping or gaps in your ranges.
You can try a recursive CTE.
WITH CTE AS
(
SELECT Id, 0 MinB, 100 MaxB, [Range]
FROM YourTable
UNION ALL
SELECT Id, CASE WHEN MinB = 0 THEN MinB+101 ELSE MinB+100 END, MaxB + 100, [Range]
FROM CTE
WHERE MinB < [Range]
)
SELECT Id,
CAST(MinB AS VARCHAR) + ' - ' + CAST(CASE WHEN MaxB>[Range] THEN [Range] ELSE MaxB END AS VARCHAR) [Range]
FROM CTE
WHERE MinB < [Range]
ORDER BY Id, [Range]
OPTION(MAXRECURSION 5000)
I put a limit to the recursion level on 5000, but you can change it (or leave it at zero, that means basically to keep doing recursion until it can)