Oracle SQL join date dimension table with another table on date value - sql

I have a date dimension table containing all dates and another table containing the value of items at specific dates.
E.g
(a) Date_Dim table
|Full_Date |
|-----------|
| .... |
|1-jan-2021 |
|2-Jan-2021 |
|3-jan-2021 |
| ... |
(b) Item_value table
|P_Date | ITEM | Value |
|-----------:|:------|-------:|
|20-Dec-2020 |AA1 |9 |
|1-jan-2021 |AA1 |10 |
|1-jan-2021 |AA2 |100 |
| ... | ... | ... |
I am trying to build a fact table containing the latest value of every item in the item_value table for every date in the date_dim table. i.e the value of the items every day.
e.g
|Full_date | ITEM | Value |
|-----------:|-------:|------:|
|31-Dec-2020 |AA1 | 9 |
|31-Dec-2020 |AA2 | null |
|1-Jan-2021 |AA1 | 10 |
|1-Jan-2021 |AA2 | 100 |
|2-Jan-2021 |AA1 | 10 |
|2-Jan-2021 |AA2 | 100 |
|3-Jan-2021 |AA1 | 10 |
|3-Jan-2021 |AA2 | 100 |
|4-Jan-2021 |AA1 | 10 |
|4-Jan-2021 |AA2 | 100 |
How can this query be built, please?
I have tried the following but not working
select full_date,p_date,item,value
from dim_date
left outer join item_value on full_date=p_date;
Not sure whether max(p_date) over (partition by ...) will work.
Thank you

You can use a partitioned outer join and then aggregate:
WITH date_dim ( full_date ) AS (
SELECT DATE '2020-12-31' + LEVEL - 1 AS full_Date
FROM DUAL
CONNECT BY DATE '2020-12-31' + LEVEL - 1 <= DATE '2021-01-04'
)
SELECT item,
full_date,
MAX( value ) KEEP ( DENSE_RANK LAST ORDER BY p_date ) AS value
FROM date_dim d
LEFT OUTER JOIN item_value i
PARTITION BY ( i.item )
ON ( d.full_date >= i.p_date )
GROUP BY item, full_date
Which, for the sample data:
CREATE TABLE item_value ( P_Date, ITEM, Value ) AS
SELECT DATE '2020-12-20', 'AA1', 9 FROM DUAL UNION ALL
SELECT DATE '2021-01-01', 'AA1', 10 FROM DUAL UNION ALL
SELECT DATE '2021-01-01', 'AA2', 100 FROM DUAL;
Outputs:
ITEM | FULL_DATE | VALUE
:--- | :-------- | ----:
AA1 | 31-DEC-20 | 9
AA1 | 01-JAN-21 | 10
AA1 | 02-JAN-21 | 10
AA1 | 03-JAN-21 | 10
AA1 | 04-JAN-21 | 10
AA2 | 31-DEC-20 | null
AA2 | 01-JAN-21 | 100
AA2 | 02-JAN-21 | 100
AA2 | 03-JAN-21 | 100
AA2 | 04-JAN-21 | 100
Note: You do not need to store the date_dim dimension table; it can be generated on-the-fly and will reduce the need to perform (expensive) IO operations reading the table from the hard disk.
db<>fiddle here

You may simple add a validity interval for you ITEM table using the analtical function LEAD
select
P_DATE,
lead(P_DATE-1,1,(select max(full_date) from date_dim)) over (partition by ITEM order by P_DATE) P_DATE_TO,
ITEM, VALUE
from item_value
;
P_DATE P_DATE_TO ITE VALUE
------------------- ------------------- --- ----------
20.12.2020 00:00:00 31.12.2020 00:00:00 AA1 9
01.01.2021 00:00:00 04.01.2021 00:00:00 AA1 10
01.01.2021 00:00:00 04.01.2021 00:00:00 AA2 100
In some case this is enough for your use case as you can query the VALUE of a specific ITEM on a given date with
select VALUE from item_value_hist h where ITEM = 'AA2'
and <query_date> BETWEEN h.P_DATE and h.P_DATE_TO
Note, that the validity interval is inclusive, as we for P_DATE_TO subtract one day from the adjacent P_DATE. You should take some care is the DATEs have a time component.
If you want the ITEM per DAY overview you must first add the missing early history with the VALUE of NULL
select
(select min(full_date) from date_dim) P_DATE, min(P_DATE)-1 P_DATE_TO, ITEM, null VALUE
from item_value
group by ITEM
having min(P_DATE) > (select min(full_date) from date_dim)
P_DATE P_DATE_TO ITE VALUE
------------------- ------------------- --- -----
31.12.2020 00:00:00 31.12.2020 00:00:00 AA2
Than simple outer join to your dimension table matching all day from your validity interval
with item as (
select
P_DATE,
lead(P_DATE-1,1,(select max(full_date) from date_dim)) over (partition by ITEM order by P_DATE) P_DATE_TO,
ITEM, VALUE
from item_value
union all
select
/* add the missing early history without a VALUE */
(select min(full_date) from date_dim) P_DATE, min(P_DATE)-1 P_DATE_TO, ITEM, null VALUE
from item_value
group by ITEM
having min(P_DATE) > (select min(full_date) from date_dim)
)
select dt.full_date, item.ITEM, item.VALUE from item
join date_dim dt
on dt.full_date between item.P_DATE and item.P_DATE_TO
order by item.ITEM, dt.full_date
FULL_DATE ITE VALUE
------------------- --- ----------
31.12.2020 00:00:00 AA1 9
01.01.2021 00:00:00 AA1 10
02.01.2021 00:00:00 AA1 10
03.01.2021 00:00:00 AA1 10
04.01.2021 00:00:00 AA1 10
31.12.2020 00:00:00 AA2
01.01.2021 00:00:00 AA2 100
02.01.2021 00:00:00 AA2 100
03.01.2021 00:00:00 AA2 100
04.01.2021 00:00:00 AA2 100

Two steps:
Cross join dates and items. If you don't have an item table (which you should), join distinct items from your item_value table.
Get the value in the FROM clause with OUTER APPLY or in the SELECT clause with a subquery using FETCH FIRST ROW ONLY.
The query:
select
d.full_date,
i.item,
(
select iv.value
from Item_value iv
where iv.item = i.item
and iv.p_date <= d.full_date
order by iv.p_date desc
fetch first row only
) as value
from dim_date d
cross join (select distinct item from item_value) i
order by d.full_date, i.item;

You can generate the full list of dates and items using cross join followed by a left join to bring in the existing values. Then you can use last_value() or lag() to fill in the values:
select d.p_date, i.item,
coalesce(v.value,
lag(v.value ignore nulls) over (partition by i.item order by d.p_date)
) as value
from date_dim d cross join
(select distinct iv.item from item_value iv) i left join
item_value iv
on iv.p_date = d.p_date and iv.item = i.item;
You can also do this using a join by adding an "end" date to the values table:
select d.p_date, i.item,
coalesce(v.value,
lag(v.value ignore nulls) over (partition by i.item order by d.p_date)
) as value
from date_dim d cross join
(select distinct iv.item from item_value iv) i left join
(select iv.*,
lead(p_date) over (partition by item order by p_date) as next_p_date
from item_value iv
) iv
on i.item = iv.item and
d.p_date >= iv.p_date and
(iv.next_p_date is null or d.p_date < iv.next_p_date);

Related

Joining two tables in SQL to get the SUM between two dates

I'm new to SQL and this website so apologies if anything is unclear.
Basically, I got two separate tables:
Table A:
CustomerID | PromoStart | PromoEnd
1 | 2020-05-01 | 2020-05-30
2 | 2020-06-01 | 2020-07-30
3 | 2020-07-01 | 2020-10-15
Table B:
CustomerID | Date | Payment |
1 | 2020-02-15 | 5000 |
1 | 2020-05-04 | 200 |
1 | 2020-05-28 | 100 |
1 | 2020-06-05 | 1000 |
2 | 2020-06-10 | 20 |
2 | 2020-07-25 | 500 |
2 | 2020-08-02 | 1000 |
3 | 2020-09-05 | 580 |
3 | 2020-12-01 | 20 |
What I want is to get the sum of all payments that fall between PromoStart and PromoEnd for each customer.
so the desired result would be :
CustomerID | TotalPayments
1 | 300
2 | 520
3 | 580
I guess this would involve an inner (left?) join and a where clause however I just can't figure it out.
A LATERAL join would do it:
SELECT a.customer_id, b.total_payments
FROM table_a a
LEFT JOIN LATERAL (
SELECT sum(payment) AS total_payments
FROM table_b
WHERE customer_id = a.customer_id
AND date BETWEEN a.promo_start AND a.promo_end
) b ON true;
This assumes inclusive lower and upper bounds, and that you want to include all rows from table_a, even without any payments in table_b.
You can use a correlated subquery or join with aggregation. The correlated subquery looks like:
select a.*,
(select sum(b.payment)
from b
where b.customerid = a.customerid and
b.date >= a.promostart and
b.date <= a.promoend
) as totalpayments
from a;
You don't mention your database, but this can take advantage of an index on b(customerid, date, payment). By avoiding the outer aggregation, this would often have better performance than an alternative using group by.
I hope I didn't overlook something important but it seems to me simple join on range matching condition should be sufficient:
with a (CustomerID , PromoStart , PromoEnd) as (values
(1 , date '2020-05-01' , date '2020-05-30'),
(2 , date '2020-06-01' , date '2020-07-30'),
(3 , date '2020-07-01' , date '2020-10-15')
), b (CustomerID , d , Payment ) as (values
(1 , date '2020-02-15' , 5000 ),
(1 , date '2020-05-04' , 200 ),
(1 , date '2020-05-28' , 100 ),
(1 , date '2020-06-05' , 1000 ),
(2 , date '2020-06-10' , 20 ),
(2 , date '2020-07-25' , 500 ),
(2 , date '2020-08-02' , 1000 ),
(3 , date '2020-09-05' , 580 ),
(3 , date '2020-12-01' , 20 )
)
select a.CustomerID, sum(b.Payment)
from a
join b on a.CustomerID = b.CustomerID and b.d between a.PromoStart and PromoEnd
group by a.CustomerID
Db fiddle here.

Find first record of multiple values in single query

Table
timestamp | tracker_id | position
----------------------------------+------------+----------
2020-02-01 21:53:45.571429+05:30 | 15 | 1
2020-02-01 21:53:45.857143+05:30 | 11 | 1
2020-02-01 21:53:46.428571+05:30 | 15 | 1
2020-02-01 21:53:46.714286+05:30 | 11 | 2
2020-02-01 21:53:54.714288+05:30 | 15 | 2
2020-02-01 21:53:55+05:30 | 12 | 1
2020-02-01 21:53:55.285714+05:30 | 11 | 1
2020-02-01 21:53:55.571429+05:30 | 15 | 3
2020-02-01 21:53:55.857143+05:30 | 13 | 1
2020-02-01 21:53:56.428571+05:30 | 11 | 1
2020-02-01 21:53:56.714286+05:30 | 15 | 1
2020-02-01 21:53:57+05:30 | 13 | 2
2020-02-01 21:53:58.142857+05:30 | 12 | 2
2020-02-01 21:53:58.428571+05:30 | 20 | 1
Output
timestamp | tracker_id | position
----------------------------------+------------+----------
2020-02-01 21:53:45.571429+05:30 | 15 | 1
2020-02-01 21:53:45.857143+05:30 | 11 | 1
2020-02-01 21:53:55+05:30 | 12 | 1
How do I find the first record WHERE tracker_id IN ('15', '11', '12') in a single query?
I can find the first record by separately querying for each tracker_id:
SELECT *
FROM my_table
WHERE tracker_id = '15'
ORDER BY timestamp
LIMIT 1;
In Postgres this can be done using the DISTINCT ON () clause:
select distinct on (tracker_id) *
from the_table
where tracker_id in (11,12,15)
order by tracker_id, "timestamp" desc;
Online example
I have named your timestampl column col1 because I do nto recommend to name your columns with keywords.
select * from mytable m
where m.col1 = (select min(col1)
from mytable m1
where m.tracker_id = m1.tracker_id
group by tracker_id)
and m.tracker_id in (11,15,12);
Here is a small demo
You can use first_value with the nested select query:
select mt.*
from my_table mt
where mt.timestamp in (
select first_value(imt.timestamp) over (partition by imt.tracker_id order by imt.timestamp)
from my_table imt
where imt.tracker_id in ('11', '12', '15')
)
I'm assuming timestamp is unique, like you said in the comment. You can always replace the joining column with a primary key, like id.
select distinct on (tracker_id) *
from the_table
where tracker_id in ( select distinct tracker_id from the_table)
order by tracker_id, "timestamp" desc;
If you want the first row that matches each of your IN values, you can use a window function:
SELECT src.timestamp, src.tracker_id, src.position
FROM (
SELECT
t.timestamp, t.tracker_id, t.position,
ROW_NUMBER() OVER(PARTITION BY tracker_id ORDER BY timestamp DESC) myrownum
FROM mytable t
WHERE tracker_id IN ('15', '11', '12')
) src
WHERE myrownum = 1 -- Get first row for each "tracker_id" grouping
This will return the first row that matches for each of your IN values, ordering by timestamp.
Find this Query:
You can uncomment where clause if you want to run query for selected tracker_id
;WITH CTE AS
(
SELECT ROW_NUMBER() OVER (PARTITION BY tracker_id ORDER BY timestamp)
duplicates, * FROM my_table -- WHERE tracker_id IN (15,11,12)
)
SELECT timestamp, tracker_id, position FROM CTE WHERE duplicates = 1
select distinct on (tracker_id) *
from table
where tracker_id in (11,12,15)
order by tracker_id, "timestamp" asc;
i use distinct on when use postgres for this case

Update end date based on its succeeding start date in sql server

I am new to SQL Server, I tried few methods but couldn't able to get succeed to update below nulls with the value of their immediate successive to respective products (start_day-1 day), It is my production scenario, so I cant able to publish original query I tried. So kindly help me to achieve this scenario.
Table_Name - Product
Actual data:
------------------------------------------
Product_cd | Start_date | end_date
------------------------------------------
A | 2017-01-01 | 2017-01-10
A | 2017-01-11 | null
A | 2017-03-10 | 2099-12-31
B | 2015-01-01 | null
B | 2017-01-11 | 2099-12-31
C | 2015-01-01 | 2015-01-10
C | 2015-01-11 | null
C | 2015-03-10 | 2015-03-09
C | 2015-03-10 | 2099-12-31
D | 2000-01-01 | 2000-10-21
D | 2000-10-22 | 2000-11-12
D | 2000-11-13 | null
D | 2015-03-10 | 2099-12-31
Correct data expecting: (After Null in end_date, min(start_date) for same product- 1 day)
------------------------------------------
Product_cd | Start_date | end_date
------------------------------------------
A | 2017-01-01 | 2017-01-10
A | 2017-01-11 | 2017-03-09
A | 2017-03-10 | 2099-12-31
B | 2015-01-01 | 2017-01-10
B | 2017-01-11 | 2099-12-31
C | 2015-01-01 | 2015-01-10
C | 2015-01-11 | 2015-03-09
C | 2015-03-10 | 2015-03-09
C | 2015-03-10 | 2099-12-31
D | 2000-01-01 | 2000-10-21
D | 2000-10-22 | 2000-11-12
D | 2000-11-13 | 2015-03-09
D | 2015-03-10 | 2099-12-31
As etsa says the LEAD window function is what you need to use here (see here). You can only put this in a SELECT though so your update will need to be via something like a CTE. Try something like this...
DROP TABLE IF EXISTS StartEnd
CREATE TABLE StartEnd
( Product_cd char(1),
Startdate date,
end_date date
)
INSERT dbo.StartEnd (Product_cd,Startdate,end_date)
VALUES
('A','2017-01-01','2017-01-10' ),
('A','2017-01-11',null ),
('A','2017-03-10','2099-12-31' ),
('B','2015-01-01',null ),
('B','2017-01-11','2099-12-31' ),
('C','2015-01-01','2015-01-10' ),
('C','2015-01-11',null ),
('C','2015-03-10','2015-03-09' ),
('C','2015-03-10','2099-12-31' ),
('D','2000-01-01','2000-10-21' ),
('D','2000-10-22','2000-11-12' ),
('D','2000-11-13',null ),
('D','2015-03-10','2099-12-31' );
SELECT * FROM dbo.StartEnd AS se;
WITH UpdateRows AS
(
SELECT se.Product_cd,
se.Startdate,
se.end_date,
CASE WHEN se.end_date IS NULL
THEN dateadd(DAY,-1,lead(se.StartDate,1) OVER(PARTITION BY se.Product_cd ORDER BY se.Startdate))
ELSE se.end_date END AS newEndDate
FROM dbo.StartEnd AS se
)
UPDATE UpdateRows
SET end_date = newEndDate
WHERE end_date IS NULL;
SELECT * FROM dbo.StartEnd AS se;
In SQL Server 2012+, you can use lead(). In earlier versions, you need another method. Here is one:
update p
set end_date = dateadd(day, -1, p2.start_date)
from product p outer apply
(select top 1 p2.*
from product p2
where p2.product_cd = p.product_cd and
p2.start_date > p.start_date
order by p2.start_date desc
) p2
where p.end_date is null;
If you just want to retrieve the data, then you can use the same from clause in a select.
Try this.....
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT 1)) rownum,* INTO #Temp_table
FROM dbo.StartEnd f1
SELECT t1.Product_cd,t1.Startdate,DATEADD(DAY,-1,t2.Startdate)end_date
FROM #Temp_table t1
LEFT JOIN #Temp_table t2 ON t1.rownum = t2.rownum - 1
To extract the values you want you can use following query. It use windows analytical function LEAD() to find next value for a PRODUCT_CD, using START_DATE ordering). (As Gordon pointed out, in MSSQL 2012+)
SELECT *
FROM (SELECT PRODUCT_CD, START_DATE, END_DATE
, LEAD(START_DATE) OVER (PARTITION BY PRODUCT_CD ORDER BY START_DATE)-1 AS DATE_SUCC
FROM PRODUCT) A
WHERE END_DATE IS NULL AND DATE_SUCC IS NOT NULL;
Try to make the UPDATE by yourself. If you find any problem let me know and we'll see together.
I thought it would be useful for you to try to do the UPDATE, but others don't think so.
Here is the UPDATE, starting from my SELECT (I don't think CTE is necessary). I used it inside a BEGIN TRAN / ROLLBACK TRAN, so you can check it.
BEGIN TRAN
UPDATE A SET END_DATE = A.DATE_SUCC
FROM (SELECT PRODUCT_CD, START_DATE, END_DATE
, LEAD(START_DATE) OVER (PARTITION BY PRODUCT_CD ORDER BY START_DATE)-1 AS DATE_SUCC
FROM PRODUCT) A
WHERE A.END_DATE IS NULL AND A.DATE_SUCC IS NOT NULL
SELECT * FROM PRODUCT
ROLLBACK TRAN
Output sample:
PRODUCT_CD START_DATE END_DATE
A 2017-01-01 00:00:00.000 2017-01-10 00:00:00.000
A 2017-01-11 00:00:00.000 2017-03-09 00:00:00.000
A 2017-03-10 00:00:00.000 2099-12-31 00:00:00.000
B 2015-01-01 00:00:00.000 2017-01-10 00:00:00.000
B 2017-01-11 00:00:00.000 2099-12-31 00:00:00.000
...

How can I join a table to get values similar to the Excel VLOOKUP-Range-Function?

I'm using HP Vertica to write my queries. I want to select some data which should look like Excel would do it when you use the VLOOKUP function with the range flag enabled [ VLOOKUP(A1;B1:C4;2;1) ].
I give you one simple example for better understanding. I have a table showing historic warehouse movements.
stock_history
-------------
|product|location|time_stamp |
|-------|--------|------------|
| A | Loc A | 2015-01-13 |
| A | Loc B | 2015-03-13 |
Product A was moved in location A in January
(and stayed there in February)
and was moved in location B in March
Now I want to see the Location of A at every month (let's say there is only one movement allowed per month to make it easier)
It should look like this
|product|location|month |
|-------|--------|----- ---|
| A | Loc A | 2015-01 |
| A | Loc A | 2015-02 |
| A | Loc B | 2015-03 |
I've generated a table which shows all months:
all_months
----------
|month |
|---------|
| 2015-01 |
| 2015-02 |
| 2015-03 |
Here is a statement I tried
select his.product
, his.location
, mon.month
from stock_history as his
left outer join all_months as mon
on mon.month = to_char( time_stamp, 'YYYY-MM' )
|product |location|month |
|--------|--------|----- ---|
| A | Loc A | 2015-01 |
| (null) | (null) | 2015-02 |
| A | Loc B | 2015-03 |
How do I manage it to get the product A also in the February-line, because it still was in location A in February?
Thanks for reading my question. I'm looking forward to get your answers ;)
Regards,
Felix
Here you go !
I have also added example with added months.Made use of recursive features.
I tested with oracle, should work with vertica also.
CREATE TABLE A
(PRODUCT CHAR(1),LOCATION VARCHAR(10),MONTHS VARCHAR(10))
INSERT INTO A (PRODUCT,LOCATION,MONTHS)
SELECT 'A','LOC A','2015-01' FROM DUAL
UNION
SELECT 'A','LOC B','2015-03' FROM DUAL
CREATE TABLE MONTHS
(MON VARCHAR(10))
INSERT INTO MONTHS(MON)
SELECT '2015-01' FROM DUAL
UNION
SELECT '2015-02' FROM DUAL
UNION
SELECT '2015-03' FROM DUAL
UNION
SELECT '2015-04' FROM DUAL
UNION
SELECT '2015-05' FROM DUAL
UNION
SELECT '2015-06' FROM DUAL
COMMIT
WITH CTE (I,PRODUCT,LOCATION,MON) AS
(
SELECT 1 I,BASE.PRODUCT,A.LOCATION,M.MON
FROM
(SELECT DISTINCT PRODUCT FROM A)BASE
CROSS JOIN
MONTHS M
LEFT JOIN A
ON A.MONTHS=M.MON
UNION ALL
SELECT I+1,PRODUCT,COALESCE(LOCATION,LAG(LOCATION)OVER(PARTITION BY PRODUCT ORDER BY MON)) AS LOC,MON
FROM
CTE WHERE I<12
)
SELECT DISTINCT PRODUCT,LOCATION,MON FROM CTE WHERE LOCATION IS NOT NULL
ORDER BY MON
You can generate all the month/product combinations using a cross join. Then use a correlated subquery to get the location from the most recent or current month:
select mon.month, p.product,
(select sh.location
from stock_history sh
where mon.month <= to_char(sh.time_stamp, 'YYYY-MM' ) and p.product = sh.product
order by mon.month desc
limit 1
) as location
from (select distinct product p from stock_history) p cross join
all_months mon;

Remove duplicate rows query result except for one in Microsoft SQL Server?

How would I delete all duplicate month from a Microsoft SQL Server Table?
For example, with the following syntax I just created:
SELECT * FROM Cash WHERE Id = '2' AND TransactionDate between '2014/07/01' AND '2015/02/28'
and the query result is:
+----+-------------------------+
|Id | TransactionDate |
+----+-------------------------+
| 2 | 2014-07-22 00:00:00.000 |
| 2 | 2014-08-09 00:00:00.000 |
| 2 | 2014-08-25 00:00:00.000 |
| 2 | 2014-08-29 00:00:00.000 |
| 2 | 2015-01-27 00:00:00.000 |
| 2 | 2015-01-28 00:00:00.000 |
+----+-------------------------+
How would I remove duplicates month which is only return any 1 value for any 1 month each, like this result:
+----+-------------------------+
|Id | TransactionDate |
+----+-------------------------+
| 2 | 2014-07-22 00:00:00.000 |
| 2 | 2014-08-09 00:00:00.000 |
| 2 | 2015-01-27 00:00:00.000 |
+----+-------------------------+
You can do it with the help of ROW_NUMBER.
This will tell you which are the rows you are going to keep
SELECT id,transactionDate, ROW_NUMBER() OVER ( PARTITION BY YEAR(TransactionDate ),MONTH(TransactionDate ) ORDER BY TransactionDate ) firstTrans
FROM Cash
WHERE Id = '2' AND
TransactionDate between '2014/07/01' AND '2015/02/28'
You can delete the other rows with a CTE.
with myCTE (id,transactionDate, firstTrans) AS (
SELECT id,transactionDate, ROW_NUMBER() OVER ( PARTITION BY YEAR(TransactionDate ),MONTH(TransactionDate ) ORDER BY TransactionDate ) firstTrans
FROM Cash
WHERE Id = '2' AND
TransactionDate between '2014/07/01' AND '2015/02/28'
)
delete from myCTE where firstTrans <> 1
Will only keep one transaction for each month of each year.
EDIT:
filter by the row_number and will only return the rows you want
select id, transactionDate from (SELECT id,transactionDate, ROW_NUMBER() OVER ( PARTITION BY YEAR(TransactionDate ),MONTH(TransactionDate ) ORDER BY TransactionDate ) firstTrans
FROM Cash
WHERE Id = '2' AND
TransactionDate between '2014/07/01' AND '2015/02/28') where firstTrans = 1
When you run this query you will get the highest Id for each month in each year.
SELECT MAX(<IdColumn>) AS Id, YEAR(<DateColumn>) AS YE, MONTH(<DateColumn>) AS MO FROM <YourTable>
GROUP BY YEAR(<DateColumn>), MONTH(<DateColumn>)
If needed, for example, you can late delete rows that their Id is not in this query.
Select only the first row per month
SELECT *
FROM Cash c
WHERE c.Id = '2'
AND c.TransactionDate between '2014/07/01' AND '2015/02/28'
AND NOT EXISTS ( SELECT 'a'
FROM Cash c2
WHERE c2.Id = c.Id
AND YEAR(c2.TransactionDate) * 100 + MONTH(c2.TransactionDate) = YEAR(c.TransactionDate) * 100 + MONTH(c.TransactionDate)
AND c2.TransactionDate < c.TransactionDate
)