merging multiple queries that have the same core syntax - sql

Sorry for the vague title!
I'm building a web filter where users will be able to click on options and filter down results using MS-SQL 2012
The problem I have is i'm running 4 queries on every selection made to build the filter and 1 for the results.
Ignoring how it can be best coded so I don't have to reload the filter queries every time, I need help in how i can merge the 4 queries that produce the filter options and counts into 1.
The core of the syntax is the same where most of the logic goes to extract the results based on the filtered selection. However, i need to still produce the filters with their counts.
select datePart(yy,p.startDate) year, count(p.personId) itemCount
from person p
where p.field1 = something
and p.field2 = somethingElse...
Is there a way of running the core and producing the filter list and counts (see below) all in one rather than doing each individually?
Below is an example of 2 of the filters, but there are others that do similar things, either produce a list from existing data or produce a list from based on before, between and after certain dates.
--Filter 1 to get years and counts
with annualList as
(
select a.year
from table a
where a.year > 2000
)
select al.year, isnull(count,0) itemCount
from annualList al
left join (
select datePart(yy,p.startDate) year, count(p.personId) itemCount
from person p
where p.field1 = something
and p.field2 = somethingElse...
) group by datePart(yy,p.startDate) persons
on al.year = persons.year
order by al.year desc;
--filter 2 to get group stats
select anotherList.groupStatus, groupStatusCounts.itemCount
from (
select 'Child' as groupStatus
union all
select 'Adult' as groupStatus
union all
select 'Pensioner' as groupStatus
) anotherList
left join (
SELECT personStatus.groupStatus, count(personStatus.personId) itemCount
FROM ( select p.personId
case when (p.age between 1 and 17) then 'Child'
case when (p.age between 18 and 67) then 'Adult'
case when (p.age > 65) then 'Pensioner'
end as groupStatus
FROM person p
--and some other syntax to calculate the age...
where p.field1 = something
and p.field2 = somethingElse exactly as above query...
) personStatus
GROUP BY personStatus.groupStatus
) groupStatusCounts
ON anotherList.groupStatus = groupStatusCounts.groupStatus
As an example, using the dataset below, from regDate and using a group by I will be able to to get a list of years from 2010-2014 (filter1 code above).
Using the dataofbirth i'll need to 'calculate' the groupStatus using a case. As you can see from the dataofbirth data, I don't have any records where I can identify a pensioner hence the way I wrote filter 2 (see code above), it will give me the filters I need even if I don't have the data to represent it.
Tried to add the code on SQL Fiddle unfortunately it was down last night
INSERT INTO personTable
([PersonID], [dateOfBirth], [regDate])
VALUES
(1, '1979-01-01 00:00:00', '2010-01-01 00:00:00'),
(2, '1979-01-01 00:00:00', '2010-01-01 00:00:00'),
(3, '1979-01-01 00:00:00', '2011-01-01 00:00:00'),
(4, '1979-01-01 00:00:00', '2011-01-01 00:00:00'),
(5, '1979-01-01 00:00:00', '2012-01-01 00:00:00'),
(6, '1979-01-01 00:00:00', '2012-01-01 00:00:00'),
(7, '1984-01-01 00:00:00', '2012-01-01 00:00:00'),
(8, '1992-01-01 00:00:00', '2012-01-01 00:00:00'),
(9, '2000-01-01 00:00:00', '2013-01-01 00:00:00'),
(10, '2010-01-01 00:00:00', '2014-01-01 00:00:00')
my example of SQL Fiddle
The result want to end up with I need it to look like this based on the results shown in Fiddle
filter | Year | groupStatus
-----------+-----------+--------
2010 | 0 | null
2011 | 2 | null
2012 | 3 | null
2013 | 0 | null
2014 | 3 | null
child | null | 2
adult | null | 6
pensioner | null | 0
Thanking you in advance

I may have misunderstood the requirement massively, however I suspect you want to use GROUPING SETS. As a simple example (ignoring your specific filters) imagine the following simple data set:
PersonID | GroupStatus | Year
---------+-------------+--------
1 | Adult | 2013
2 | Adult | 2013
3 | Adult | 2014
4 | Adult | 2014
5 | Adult | 2014
6 | Child | 2012
7 | Child | 2014
8 | Pensioner | 2012
9 | Pensioner | 2013
10 | Pensioner | 2013
From what I gather you are trying to get 2 different summaries out of this data without repeating the query, e.g.
GroupStatus | ItemCount
------------+-----------
Adult | 5
Child | 2
Pensioner | 3
And
Year | ItemCount
------------+-----------
2012 | 2
2013 | 4
2014 | 4
You can get this in a single data set using:
SELECT Year,
GroupStatus,
ItemCount = COUNT(*)
FROM T
GROUP BY GROUPING SETS ((Year), (GroupStatus));
Which will yield a single data set:
YEAR GROUPSTATUS ITEMCOUNT
------------------------------------
NULL Adult 5
NULL Child 2
NULL Pensioner 3
2012 NULL 2
2013 NULL 4
2014 NULL 4
If you wanted to include the full data as well, you can just include a grouping set with all fields e.g. (PersonID, Year, GroupStatus).
Examples on SQL Fiddle
EDIT
The below query gives the output you have requested:
WITH AllValues AS
( SELECT TOP (DATEPART(YEAR, GETDATE()) - 2009)
Filter = CAST(2009 + ROW_NUMBER() OVER(ORDER BY object_id) AS VARCHAR(15)),
FilterType = 'Year'
FROM sys.all_objects o
UNION ALL
SELECT Filter, 'GroupStatus'
FROM (VALUES ('child'), ('Adult'), ('Pensioner')) T (Filter)
)
SELECT v.Filter,
[Year] = CASE WHEN v.FilterType = 'Year' THEN ISNULL(data.ItemCount, 0) END,
GroupStatus = CASE WHEN v.FilterType = 'GroupStatus' THEN ISNULL(data.ItemCount, 0) END
FROM AllValues AS v
LEFT JOIN
( SELECT [Year] = DATENAME(YEAR, t.RegDate),
Age = a.Name,
ItemCount = COUNT(*)
FROM T
LEFT JOIN
(VALUES
(0, 18, 'Child'),
(18, 65, 'adult'),
(65, 1000000, 'pensioner')
) a (LowerValue, UpperValue, Name)
ON a.LowerValue <= DATEDIFF(HOUR, T.dateofbirth, GETDATE()) / 8766.0
AND a.UpperValue > DATEDIFF(HOUR, T.dateofbirth, GETDATE()) / 8766.0
WHERE T.PersonID > 2
GROUP BY GROUPING SETS ((DATENAME(YEAR, t.RegDate)), (a.Name))
) AS data
ON ISNULL(data.[Year], data.Age) = v.Filter;
Example on SQL Fiddle

Related

Daily record count based on status allocation

I have a table named Books and a table named Transfer with the following structure:
CREATE TABLE Books
(
BookID int,
Title varchar(150),
PurchaseDate date,
Bookstore varchar(150),
City varchar(150)
);
INSERT INTO Books VALUES (1, 'Cujo', '2022-02-01', 'CentralPark1', 'New York');
INSERT INTO Books VALUES (2, 'The Hotel New Hampshire', '2022-01-08', 'TheStrip1', 'Las Vegas');
INSERT INTO Books VALUES (3, 'Gorky Park', '2022-05-19', 'CentralPark2', 'New York');
CREATE TABLE Transfer
(
BookID int,
BookStatus varchar(50),
TransferDate date
);
INSERT INTO Transfer VALUES (1, 'Rented', '2022-11-01');
INSERT INTO Transfer VALUES (1, 'Returned', '2022-11-05');
INSERT INTO Transfer VALUES (1, 'Rented', '2022-11-06');
INSERT INTO Transfer VALUES (1, 'Returned', '2022-11-09');
INSERT INTO Transfer VALUES (2, 'Rented', '2022-11-03');
INSERT INTO Transfer VALUES (2, 'Returned', '2022-11-09');
INSERT INTO Transfer VALUES (2, 'Rented', '2022-11-15');
INSERT INTO Transfer VALUES (2, 'Returned', '2022-11-23');
INSERT INTO Transfer VALUES (3, 'Rented', '2022-11-14');
INSERT INTO Transfer VALUES (3, 'Returned', '2022-11-21');
INSERT INTO Transfer VALUES (3, 'Rented', '2022-11-25');
INSERT INTO Transfer VALUES (3, 'Returned', '2022-11-29');
See fiddle.
I want to do a query for a date interval (in this case 01.11 - 09.11) that returns the book count for each day based on BookStatus from Transfer, like so:
+────────────+────────+────────+────────+────────+────────+────────+────────+────────+────────+
| Status | 01.11 | 02.11 | 03.11 | 04.11 | 05.11 | 06.11 | 07.11 | 08.11 | 09.11 |
+────────────+────────+────────+────────+────────+────────+────────+────────+────────+────────+
| Rented | 2 | 1 | 2 | 2 | 0 | 2 | 3 | 3 | 1 |
+────────────+────────+────────+────────+────────+────────+────────+────────+────────+────────+
| Returned | 1 | 2 | 1 | 1 | 3 | 1 | 0 | 0 | 2 |
+────────────+────────+────────+────────+────────+────────+────────+────────+────────+────────+
A book remains rented as long as it was not returned, and is counted as 'Returned' every day until it is rented out again.
This is what the query result would look like for one book (BookID 1):
I see two possible solutions.
Dynamic solution
Use a (recursive) common table expression to generate a list of all the dates that fall within the requested range.
Use two cross apply statements that each perform a count() aggregation function to count the amount of book transfers.
-- generate date range
with Dates as
(
select convert(date, '2022-11-01') as TransferDate
union all
select dateadd(day, 1, d.TransferDate)
from Dates d
where d.TransferDate < '2022-11-10'
)
select d.TransferDate,
c1.CountRented,
c2.CountReturned
from Dates d
-- count all rented books up till today, that have not been returned before today
cross apply ( select count(1) as CountRented
from Transfer t1
where t1.BookStatus = 'Rented'
and t1.TransferDate <= d.TransferDate
and not exists ( select 'x'
from Transfer t2
where t2.BookId = t1.BookId
and t2.BookStatus = 'Returned'
and t2.TransferDate > t1.TransferDate
and t2.TransferDate <= d.TransferDate ) ) c1
-- count all returned books for today
cross apply ( select count(1) as CountReturned
from Transfer t1
where t1.BookStatus = 'Returned'
and t1.TransferDate = d.TransferDate ) c2;
Result:
TransferDate CountRented CountReturned
------------ ----------- -------------
2022-11-01 1 0
2022-11-02 1 0
2022-11-03 2 0
2022-11-04 2 0
2022-11-05 1 1
2022-11-06 2 0
2022-11-07 2 0
2022-11-08 2 0
2022-11-09 0 2
2022-11-10 0 0
This result is not the pivoted outcome described in the question. However, pivoting this dynamic solution requires dynamic sql, which is not trivial!
Static solution
This will delivery the exact outcome as described in the question (including the date formatting), but requires the date range to be fully typed out once.
The essential building blocks are similar to the dynamic solution above:
A recursive common table expression to generate a date range.
Two cross apply's to perform the counting calculations like before.
There is also:
An extra cross join to duplicate the date range for each BookStatus (avoid NULL values in the result).
Some replace(), str() and datepart() functions to format the dates.
A case expression to merge the two counts to a single column.
The solution is probably not the most performant, but it does deliver the requested result. If you want to validate for BookID=1 then just uncomment the extra WHERE filter clauses.
with Dates as
(
select convert(date, '2022-11-01') as TransferDate
union all
select dateadd(day, 1, d.TransferDate)
from Dates d
where d.TransferDate < '2022-11-10'
),
PivotInput as
(
select replace(str(datepart(day, d.TransferDate), 2), space(1), '0') + '.' + replace(str(datepart(month, d.TransferDate), 2), space(1), '0') as TransferDate,
s.BookStatus as [Status],
case when s.BookStatus = 'Rented' then sc1.CountRented else sc2.CountReturned end as BookStatusCount
from Dates d
cross join (values('Rented'), ('Returned')) s(BookStatus)
cross apply ( select count(1) as CountRented
from Transfer t1
where t1.BookStatus = s.BookStatus
and t1.TransferDate <= d.TransferDate
--and t1.BookID = 1
and not exists ( select 'x'
from Transfer t2
where t2.BookId = t1.BookId
and t2.BookStatus = 'Returned'
and t2.TransferDate > t1.TransferDate
and t2.TransferDate <= d.TransferDate ) ) sc1
cross apply ( select count(1) as CountReturned
from Transfer t3
where t3.TransferDate = d.TransferDate
--and t3.BookID = 1
and t3.BookStatus = 'Returned' ) sc2
)
select piv.*
from PivotInput pivi
pivot (sum(pivi.BookStatusCount) for pivi.TransferDate in (
[01.11],
[02.11],
[03.11],
[04.11],
[05.11],
[06.11],
[07.11],
[08.11],
[09.11],
[10.11])) piv;
Result:
Status 01.11 02.11 03.11 04.11 05.11 06.11 07.11 08.11 09.11 10.11
Rented 1 1 2 2 1 2 2 2 0 0
Returned 0 0 0 0 1 0 0 0 2 0
Fiddle to see things in action.

SQL performing day difference by matching value

My goal is to get the duration when the 1st OLD or 1st NEW status reaches to the 1st END. For example: Table1
ID Day STATUS
111 1 NEW
111 2 NEW
111 3 OLD
111 4 END
111 5 END
112 1 OLD
112 2 OLD
112 3 NEW
112 4 NEW
112 5 END
113 1 NEW
113 2 NEW
The desired outcome would be:
STATUS Count
NEW 2 (1 for ID 111-New on day 1 to End on day 4,and 1 for 112-new on day 3 to End on day 5)
OLD 2 (1 for ID 111-Old on day 3 to End on day 4, and 1 for 112-OLD on day 1 to End on day 5)
The following is T-SQL (SQL Server) and NOT available in MySQL. The choice of dbms is vital in a question because there are so many dbms specific choices to make. The query below requires using a "window function" row_number() over() and a common table expression neither of which exist yet in MySQL (but will one day). This solution also uses cross apply which (to date) is SQL Server specific but there are alternatives in Postgres and Oracle 12 using lateral joins.
SQL Fiddle
MS SQL Server 2014 Schema Setup:
CREATE TABLE Table1
(id int, day int, status varchar(3))
;
INSERT INTO Table1
(id, day, status)
VALUES
(111, 1, 'NEW'),
(111, 2, 'NEW'),
(111, 3, 'OLD'),
(111, 4, 'END'),
(111, 5, 'END'),
(112, 1, 'OLD'),
(112, 2, 'OLD'),
(112, 3, 'NEW'),
(112, 4, 'NEW'),
(112, 5, 'END'),
(113, 1, 'NEW'),
(113, 2, 'NEW')
;
Query 1:
with cte as (
select
*
from (
select t.*
, row_number() over(partition by id, status order by day) rn
from table1 t
) d
where rn = 1
)
select
t.id, t.day, ca.nxtDay, t.Status, ca.nxtStatus
from cte t
outer apply (
select top(1) Status, day
from cte nxt
where t.id = nxt.id
and t.status = 'NEW' and nxt.status = 'END'
order by day
) ca (nxtStatus, nxtDay)
where nxtStatus IS NOT NULL or Status = 'OLD'
order by id, day
Results:
| id | day | nxtDay | Status | nxtStatus |
|-----|-----|--------|--------|-----------|
| 111 | 1 | 4 | NEW | END |
| 111 | 3 | (null) | OLD | (null) |
| 112 | 1 | (null) | OLD | (null) |
| 112 | 3 | 5 | NEW | END |
As you can see, counting that Status column would result in NEW = 2 and OLD = 2

Cummulative SUM based on columns

I have a table with values like this:
I want to get cumulative sum based on the ID and year, so it should return an output like this i.e for id- 1 and year 2010 the sum of records will be 2.
id-2 and year 2010 the sum of records will be 1 and
id- 2 and for year 2011 it will be 1+1 = 2 i.e i require a running total for each id in ascending order based upon year.
similarly for id =3 Sum will be 1 , for id 4 will be 1 based on the year. for 5 it will be 3 for yr 2014 , for 2015 it will be sum of count previous yr + sum of count current yr i.e it will be 3 + 1 = 4 and for year 2016 it will be 3+ 1+1 = 5. Hence what is to be done. Could someone please help?
No need to make thinks more complicated than they need to be...
IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL
DROP TABLE #TestData;
CREATE TABLE #TestData (
ID INT NOT NULL,
[Year] INT NOT NULL
);
INSERT #TestData (ID, Year) VALUES
(1, 2010), (1, 2010), (2, 2010), (2, 2011),
(3, 2012), (4, 2013), (5, 2014), (5, 2014),
(5, 2014), (5, 2015), (5, 2016);
--=======================================
SELECT
tdg.ID,
tdg.Year,
RunningCount = SUM(tdg.Cnt) OVER (PARTITION BY tdg.ID ORDER BY tdg.Year ROWS UNBOUNDED PRECEDING)
FROM (
SELECT td.ID, td.Year, Cnt = COUNT(1)
FROM #TestData td
GROUP BY td.ID, td.Year
) tdg;
Results...
ID Year RunningCount
----------- ----------- ------------
1 2010 2
2 2010 1
2 2011 2
3 2012 1
4 2013 1
5 2014 3
5 2015 4
5 2016 5
this is more nesting than I would like, and I feel there is a better way to do this with maybe only one windows function but I can't get past not having a unique row for your data.
SELECT id,
year ,sum(c) OVER (
PARTITION BY id ORDER BY year rows unbounded preceding
)
FROM (
SELECT id,
year,
count(rn) c
FROM (
SELECT id,
year,
row_number() OVER (
ORDER BY year
) AS rn
FROM your_table -- you will need to change this to your table
) a
GROUP BY id,
year
) a
what we do is first build the data with a row number so now everything is unique, after that we then do a count on that unique row number and do windows function to do a running total for the count of rows by year.
There are many ways to do this. Here is one of them, with an inner query:
create table #table_name
(
UserID int,
Year int
)
INSERT INTO #table_name (UserID, Year)
VALUES
(1, 2010)
,(1,2010)
,(2,2010)
,(2,2011)
,(3,2012)
,(4,2013)
,(5,2014)
,(5,2014)
,(5,2014)
,(5,2015)
,(5,2016)
SELECT
UserID
,YEAR
,(SELECT COUNT(Year) FROM #table_name WHERE Year <= tt.Year AND UserID = tt.UserID)
FROM
#table_name AS tt
GROUP BY UserID, Year
you can also use row number over (edit : see below answer for this technique, I think it is a little bit too complicated for such a simple task). The query above returns your required output
+--------+------+-------+
| UserID | Year | COUNT |
+--------+------+-------+
| 1 | 2010 | 2 |
| 2 | 2010 | 1 |
| 2 | 2011 | 2 |
| 3 | 2012 | 1 |
| 4 | 2013 | 1 |
| 5 | 2014 | 3 |
| 5 | 2015 | 4 |
| 5 | 2016 | 5 |
+--------+------+-------+

Select rows with highest value in one column in SQL

in MySQL, I am trying to select one row for each "foreign_id". It must be the row with the highest value in column "time" (which is of type DATETIME). Can you help me how the SQL SELECT statement must look like? Thank you!
This would be really great! I am already trying for hours to find a solution :(
This is my table:
primary_id | foreign_id | name | time
----------------------------------------------------
1 | 3 | a | 2017-05-18 01:02:03
2 | 3 | b | 2017-05-19 01:02:03
3 | 3 | c | 2017-05-20 01:02:03
4 | 5 | d | 2017-07-18 01:02:03
5 | 5 | e | 2017-07-20 01:02:03
6 | 5 | f | 2017-07-18 01:02:03
And this is what the result should look like:
primary_id | foreign_id | name | time
----------------------------------------------------
3 | 3 | c | 2017-05-20 01:02:03
5 | 5 | e | 2017-07-20 01:02:03
I tried to order the intermediate result by time (descending) and then to select only the first row by using LIMIT 1. But like this I cannot get one row for each foreign_id.
Another try was to first order the intermediate result by time (descending) and then to GROUP BY foreign_id. But the GROUP BY statement seems to be executed before the ORDER BY statement (I received the rows with primary_id 1 and 4 as a result, not 3 and 5).
Try this
SELECT DISTINCT *
From my_table A
INNER JOIN (SELECT foreign_id, Max(time) AS time FROM my_table GROUP BY foreign_id) B
ON A.foreign_id = B.foreign_id AND A.time = B.time
Just add some data sample to analyze special case
CREATE TABLE Table1
(`primary_id` int, `foreign_id` int, `name` varchar(1), `time` datetime)
;
INSERT INTO Table1
(`primary_id`, `foreign_id`, `name`, `time`)
VALUES
(1, 3, 'a', '2017-05-18 01:02:03'),
(2, 3, 'b', '2017-05-19 01:02:03'),
(3, 3, 'c', '2017-05-20 01:02:03'),
(7, 3, 'H', '2017-05-20 01:02:03'),
(4, 5, 'd', '2017-07-18 01:02:03'),
(5, 5, 'e', '2017-07-20 01:02:03'),
(6, 5, 'f', '2017-07-18 01:02:03')
;
http://sqlfiddle.com/#!9/38947b/6
select d.primary_id, d.foreign_id, c.name, d.time
from table1 c inner join (
select max(b.primary_id) primary_id, a.foreign_id, a.time
from table1 b inner join
( select foreign_id, max(time) time
from table1
group by foreign_id) a
on a.foreign_id = b.foreign_id and a.time=b.time
group by a.foreign_id, a.time ) d
on c.primary_id=d.primary_id
In days gone by you would code this as a correlated subquery:
SELECT *
FROM Table1 o
WHERE primary_id = (
SELECT min (m.primary_id) FROM Table1 m
WHERE m.time= (
SELECT max (i.time) FROM Table1 i
WHERE o.foreign_id=i.foreign_id
)
)
The extra subquery handles the case of duplicate foreign_id & time values. If you were sure that time was unique for each foreign_id you could omit the middle subquery.

multiple transactions within a certain time period, limited by date range

I have a database of transactions, people, transaction dates, items, etc.
Each time a person buys an item, the transaction is stored in the table like so:
personNumber, TransactionNumber, TransactionDate, ItemNumber
What I want to do is to find people (personNumber) who, from January 1st 2012(transactionDate) until March 1st 2012 have purchased the same ItemNumber multiple times within 14 days (configurable) or less. I then need to list all those transactions on a report.
Sample data:
personNumber, TransactionNumber, TransactionDate, ItemNumber
1 | 100| 2001-01-31| 200
2 | 101| 2001-02-01| 206
2 | 102| 2001-02-11| 300
1 | 103| 2001-02-09| 200
3 | 104| 2001-01-01| 001
1 | 105| 2001-02-10| 200
3 | 106| 2001-01-03| 001
1 | 107| 2001-02-28| 200
Results:
personNumber, TransactionNumber, TransactionDate, ItemNumber
1 | 100| 2001-01-31| 200
1 | 103| 2001-02-09| 200
1 | 105| 2001-02-10| 200
3 | 104| 2001-01-01| 001
3 | 106| 2001-01-03| 001
How would you go about doing that?
I've tried doing it like so:
select *
from (
select personNumber, transactionNumber, transactionDate, itemNumber,
count(*) over (
partition by personNumber, itemNumber) as boughtSame)
from transactions
where transactionDate between '2001-01-01' and '2001-03-01')t
where boughtSame > 1
and it gets me this:
personNumber, TransactionNumber, TransactionDate, ItemNumber
1 | 100| 2001-01-31| 200
1 | 103| 2001-02-09| 200
1 | 105| 2001-02-10| 200
1 | 107| 2001-02-28| 200
3 | 104| 2001-01-01| 001
3 | 106| 2001-01-03| 001
The issue is that I don't want TransactionNumber 107, since that's not within the 14 days. I'm not sure where to put in that limit of 14 days. I could do a datediff, but where, and over what?
Alas, the window functions in SQL Server 2005 just are not quite powerful enough. I would solve this using a correlated subquery.
The correlated subquery counts the number of times that a person purchased the item within 14 days after each purchase (and not counting the first purchase).
select t.*
from (select t.*,
(select count(*)
from t t2
where t2.personnumber = t.personnumber and
t2.itemnumber = t.itemnumber and
t2.transactionnumber <> t.transactionnumber and
t2.transactiondate >= t.transactiondate and
t2.transactiondate < DATEADD(day, 14, t.transactiondate
) NumWithin14Days
from transactions t
where transactionDate between '2001-01-01' and '2001-03-01'
) t
where NumWithin14Days > 0
You may want to put the time limit in the subquery as well.
An index on transactions(personnumber, itemnumber, transactionnumber, itemdate) might help this run much faster.
If as your question states you just want to find people (personNumbers) with the specified criteria, you can do a self join and group by:
create table #tx (personNumber int, transactionNumber int, transactionDate dateTime, itemNumber int)
insert into #tx
values
(1, 100, '2001-01-31', 200),
(2, 101, '2001-02-01', 206),
(2, 102, '2001-02-11', 300),
(1, 103, '2001-02-09', 200),
(3, 104, '2001-01-01', 001),
(1, 105, '2001-02-10', 200),
(3, 106, '2001-01-03', 001),
(1, 107, '2001-02-28', 200)
declare #days int = 14
select t1.personNumber from #tx t1 inner join #tx t2 on
t1.personNumber = t2.personNumber
and t1.itemNumber = t2.itemNumber
and t1.transactionNumber < t2.transactionNumber
and datediff(day, t1.transactionDate, t2.transactionDate) between 0 and #days
group by t1.personNumber
-- if more than zero joined rows there is more than one transaction in period
having count(t1.personNumber) > 0
drop table #tx