Change value of variable in pl/sql - date

I am running a unix *_sh script but with methods that use pl/sql in it.
Method in the script:
method(){
sqlplus -s ${DB_CONNECTION} << EOF
set echo on;
set timing on;
set serveroutput on;
DECLARE
v_monthNow Date:=to_date('2018-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS');
v_charMonthNow Varchar(5):=to_char(v_monthNow,'MM');
v_charMonthBefore Varchar(5) :=to_char(v_monthNow, 'MM')-1;
BEGIN
dbms_output.Put_line('SYSDATE is '|| v_monthNow ||' || MonthNow is '|| v_charMonthNow);
IF v_charMonthBefore = '0'
set v_charMonthBefore = '12'
dbms_output.Put_line('MonthBefore is '|| v_charMonthBefore);
ELSE
dbms_output.Put_line('MonthBefore is '|| v_charMonthBefore);
END;
/
EOF
}
My question is: how do I set value of v_charMonthBefore to what I want?
Actually if sysdate is Jan 2018, i need v_monthNow=1 and v_charMonthBefore=12
if sysdate is Feb 2018, i need v_monthNow=2 and v_charMonthBefore=1
Other way to do this is not required, i just need to fix what i have now as i am not really familiar with pl/sql.
Thank you

Use CASE to detect the correct value.
My CTE is here just to create all 12 months in a year. You need lines 7 - 10.
SQL> with test as
2 (select to_number(to_char(add_months(trunc(sysdate, 'yyyy'), level - 1), 'mm')) mon
3 from dual
4 connect by level <= 12
5 )
6 select
7 mon as v_month_now,
8 case when mon = 1 then 12
9 else mon - 1
10 end as v_month_before
11 from test;
V_MONTH_NOW V_MONTH_BEFORE
----------- --------------
1 12
2 1
3 2
4 3
5 4
6 5
7 6
8 7
9 8
10 9
11 10
12 11
12 rows selected.
SQL>

Related

postgresSql PIVOT: I want to return dynamic columns

I have stored data in DB like this:
id
week
qty
product_code
related_id
1
2201
10
X000001
24
2
2202
14
X000001
24
3
2201
15
X000002
24
4
2202
25
X000002
24
5
2210
11
X000001
25
6
2244
22
X000001
26
...
I want to make an sql query with the chosen related_id to get this result:
For example if i chose the related_id = 24
product_code
2201
2202
2203
...
X000001
10
14
...
X000002
15
25
...
I want to transform all the week values of the related_id into columns with ASC order and put the right qty in front of every (product_code, week) couple.
Thanks
I got finally a solution after a few days of sql training, i post it here, it's may help someone, special thanks for #S-Man for the motivation :)
DO
$$
DECLARE
sql text;
BEGIN
WITH latest_create_date AS (
SELECT product_code, week, MAX(create_date) AS max_create_date
FROM my_table_name
WHERE related_id = 437
GROUP BY product_code, week
)
SELECT string_agg(
DISTINCT 'SUM(CASE WHEN rop.week = ' || CAST(rop.week AS text) ||
' THEN rop.qty ELSE 0 END) AS ' || rop.week || '',
', '
) INTO sql
FROM my_table_name rop
INNER JOIN latest_create_date lcd
ON rop.product_code = lcd.product_code AND rop.week = lcd.week AND rop.create_date = lcd.max_create_date;
IF sql IS NOT NULL THEN
sql := 'SELECT product_code, ' || sql ||
' FROM my_table_name WHERE related_id = 437 GROUP BY product_code';
EXECUTE sql;
END IF;
END;
$$ LANGUAGE plpgsql;

PostgreSQL query with UNIQUE values returned based on condition

Its an example of a table from PostgreSQL.
I learning the SQL query and cant find anything to help me pass this.
What I`m working to achieve is:
Return UNIQ(DISTINCT) values of WNR WHEN tdate >='2020-01-13 00:00:01.757000'
WNR tdate T1 T2 T3
2 '2020-01-06 00:05:23.229000' 8 18 15
2 '2020-01-06 00:05:23.725000' 11 4 7
2 '2020-01-06 00:05:31.578000' 19 12 6
3 '2020-01-13 00:00:01.655000' 9 9 3
3 '2020-01-13 00:00:01.757000' 5 11 16
3 '2020-01-13 00:00:05.778000' 16 17 16
4 '2020-01-20 00:00:11.925000' 18 13 4
4 '2020-01-20 00:00:12.177000' 18 3 15
4 '2020-01-20 00:00:12.694000' 7 12 7
5 '2020-01-27 00:00:04.860000' 19 3 14
5 '2020-01-27 00:00:05.056000' 14 18 8
5 '2020-01-27 00:00:05.107000' 18 7 14
Result expected should be 3,4,5
Thank you!
To select distinct values in Postgresql you can use DISTINCT clause.
From Postgresql documentation: SELECT DISTINCT eliminates duplicate rows from the result. SELECT DISTINCT ON eliminates rows that match on all the specified expressions. SELECT ALL (the default) will return all candidate rows, including duplicates. (See DISTINCT Clause below.)
SELECT DISTINCT WNR
FROM table_name
WHERE tdate >='2020-01-13 00:00:01.757000';

How to get last value with condition in postgreSQL?

I have a table in postgres with three columns, one with a group, one with a date and the last with a value.
grp
mydate
value
A
2021-01-27
5
A
2021-01-23
10
A
2021-01-15
15
B
2021-01-26
7
B
2021-01-24
12
B
2021-01-15
17
I would like to create a view with a sequence of dates and the most recent value on table for each date according with group.
grp
mydate
value
A
2021-01-27
5
A
2021-01-26
10
A
2021-01-25
10
A
2021-01-24
10
A
2021-01-23
10
A
2021-01-22
15
A
2021-01-21
15
A
2021-01-20
15
A
2021-01-19
15
A
2021-01-18
15
A
2021-01-17
15
A
2021-01-16
15
A
2021-01-15
15
B
2021-01-27
7
B
2021-01-26
7
B
2021-01-25
12
B
2021-01-24
12
B
2021-01-23
17
B
2021-01-22
17
B
2021-01-21
17
B
2021-01-20
17
B
2021-01-19
17
B
2021-01-18
17
B
2021-01-17
17
B
2021-01-16
17
B
2021-01-15
17
SQL code to generate the table:
CREATE TABLE foo (
grp char(1),
mydate date,
value integer);
INSERT INTO foo VALUES
('A', '2021-01-27', 5),
('A', '2021-01-23', 10),
('A', '2021-01-15', 15),
('B', '2021-01-26', 7),
('B', '2021-01-24', 12),
('B', '2021-01-15', 17)
I have so far managed to generate a visualization with the sequence of dates joined with the distinct groups, but I am failing to get the most recent value.
SELECT DISTINCT(foo.grp), (date_trunc('day'::text, dd.dd))::date AS mydate
FROM foo, generate_series((( SELECT min(foo.mydate) AS min
FROM foo))::timestamp without time zone, (now())::timestamp without time zone, '1 day'::interval) dd(dd)
step-by-step demo:db<>fiddle
SELECT
grp,
gs::date as mydate,
value
FROM (
SELECT
*,
COALESCE( -- 2
lead(mydate) OVER (PARTITION BY grp ORDER BY mydate) - 1, -- 1
mydate
) as prev_date
FROM foo
) s,
generate_series(mydate, prev_date, interval '-1 day') as gs -- 3
ORDER BY grp, mydate DESC -- 4
lead() window function shifts the next value of an ordered group (= partition) into the current one. The group is already defined, the order is the date. This can be used to create the required date range. Since you don't want to have the last date twice (as end of the first range and beginning of the next one) the end date stops - 1 (one day before the next group starts)
This is for the very last records of the groups: They don't have a following record, so lead() yield NULL. To avoid this, COALESCE() sets them to the current record.
Now, you can create a date range with the current and the next date value using generate_series().
Finally you can generate the required order

Defining a custom week using days of the month or year in postgresql

I am running a simple query to get weekly revenue from our sales
SELECT date_trunc('week', payment_date) AS week, sum(payment_amount)
FROM payment
WHERE payment_date BETWEEN '2010-jan-1' AND '2016-dec-31'
GROUP BY week
Now I need my week start and end date to be static for every year. All 52 weeks of the year need to be accounted for e.g.
Week 1: Jan 1-7
Week 2: Jan8-14
Week 3: Jan15-21
Week 4: Jan22-28
Week 5: Jan29-Feb4 and so forth
I did some investigation and figured out that I need a user defined function using the payment_date as argument and returning a week value. I can then call this function in the SQL query above, in place of the date_trunc() function.
How can I use an incremental loop to assign a week value to the payment_date?
Can I also use this return value in group by clause in the SQL query?
Some explanation with detailed examples will be highly appreciated since I have basic to intermediate knowledge of SQL.
---------------Edit--------------
I'm trying to use 2 functions now to take into account the leap year, where I would still want March 4th to be included in the 9th week. Ive tried to use the function by &klin and convert it to SQL, I keep getting "syntax error at or near 'int' on line 9. My code is below.
create or replace function is_leap_year(int)
returns boolean language sql as $$
select $1 % 4 = 0 and ($1 % 100 <> 0 or $1 % 400 = 0)
$$;
create or replace function week_no(timestamp)
returns int language sql as $body$
declare
y int;
day_shift int;
begin
y = extract(year from $1);
day_shift = 1 + (is_leap_year(y) and $1 > make_date(y, 2, 28))::int;
return ((extract(doy from $1)::int)- day_shift) / 7+ 1;
end
$body$;
SELECT week_no(payment_date) as week_number, sum(payment_amount)
from payment p join payment_event pe on p.payment_event_id =
pe.payment_event_id
where payment_date between '2016-jan-1' and '2017-jan-1'
and pe.payment_event_type_id != 2
group by week_number
order by week_number
First, there are problems with your requirements.
Now I need my week start and end date to be static for every year.
They can't be. Leap years happen. February 29 will either shift start and end dates one year out of every four, or you'll need to allow one week to have eight days.
All 52 weeks of the year need to be customized for . . .
I think you mean that all 52 weeks need to be accounted for. But 52 * 7 = 364. You're missing a day.
I think the simplest expression that calculates a week number from a date is (extract(doy from payment_date)::integer / 7) as week. I don't know whether it's worth putting that into a function. Instead, I might start with creating a view that uses that expression.
But a calculation won't do anything special about February 29, or about the fact that every year has more than 52 * 7 days.
I really think your best bet here is to build a table instead of using a calculation.
create table weeks (
calendar_date date primary key,
week_num integer not null
check (week_num between 1 and 53)
);
Populate it with this dates for 2016 and 2017, and with calculated weeks, to give us a starting point. (2016 was a leap year.)
insert into weeks
select
('2016-01-01'::date + (n || ' days')::interval)::date as calendar_date
, extract(doy from ('2016-01-01'::date + (n || ' days')::interval)::date)::integer / 7 + 1 as calencar_week
from generate_series (0, 730) n;
Let's look at week 9.
select *
from weeks
where week_num = 9
order by calendar_date;
calendar_date week_num
--
2016-02-25 9
2016-02-26 9
2016-02-27 9
2016-02-28 9
2016-02-29 9
2016-03-01 9
2016-03-02 9
2017-02-25 9
2017-02-26 9
2017-02-27 9
2017-02-28 9
2017-03-01 9
2017-03-02 9
2017-03-03 9
In 2016, the calculated week 9 ran from 2016-02-25 to 2016-03-02. In 2017, it ran from 2016-02-25 to 2017-03-03. But now that all these week numbers are in a table, you can adjust them any way you like. You can even change the adjustments from year to year if it makes sense to do that.
Use doy (the day of the year) in the way like this:
create or replace function week_no(date)
returns int language sql as $$
select ((extract(doy from $1)::int)- 1) / 7+ 1
$$;
with the_table(a_date) as (
values
('2017-01-01'::date),
('2017-01-07'),
('2017-01-08'),
('2017-01-14'),
('2017-01-15'),
('2017-01-22')
)
select extract(doy from a_date)::int as doy, week_no(a_date)
from the_table;
doy | week_no
-----+---------
1 | 1
7 | 1
8 | 2
14 | 2
15 | 3
22 | 4
(6 rows)
If you want to correct the week number so that March 4th is always in 9th week (even in a leap year), use this handy function:
create or replace function is_leap_year(int)
returns boolean language sql as $$
select $1 % 4 = 0 and ($1 % 100 <> 0 or $1 % 400 = 0)
$$;
Your function may look like this (I've used the plpgsql language for better readability though this also can be coded as an sql function):
create or replace function week_no_corrected(date)
returns int language plpgsql as $$
declare
y int = extract (year from $1);
day_shift int = 1 + (is_leap_year(y) and $1 > make_date(y, 2, 28))::int;
begin
return ((extract(doy from $1)::int)- day_shift) / 7+ 1;
end;
$$;
with the_table(a_date) as (
values
('2016-03-03'::date),
('2016-03-04'),
('2016-03-05'),
('2017-03-03'),
('2017-03-04'),
('2017-03-05')
)
select a_date, week_no(a_date), week_no_corrected(a_date)
from the_table;
a_date | week_no | week_no_corrected
------------+---------+-------------------
2016-03-03 | 9 | 9
2016-03-04 | 10 | 9
2016-03-05 | 10 | 10
2017-03-03 | 9 | 9
2017-03-04 | 9 | 9
2017-03-05 | 10 | 10
(6 rows)
In an SQL function you cannot use variables, assignments may be replaced by derived tables:
create or replace function week_no_corrected(date)
returns int language sql as $$
select ((extract(doy from $1)::int)- day_shift) / 7 + 1
from (
select 1 + (is_leap_year(y) and $1 > make_date(y, 2, 28))::int as day_shift
from (
select extract (year from $1)::int as y
) s
) s
$$;
By breaking your problem down to month-day strings it will allow you to use the same logic across multiple years
mysql> SELECT "01-07" < "01-08";
+-------------------+
| "01-07" < "01-08" |
+-------------------+
| 1 |
+-------------------+
1 row in set (0.08 sec)
A simple date format of %m-%d works for comparing the payment dates to the week buckets you want to assign.
To manually assign all 52 week ranges, you can use a case statement:
SET #md_format="%m-%d";
SELECT
CASE
WHEN (date_format(`input_date`, #md_format) < "01-08") THEN 1
WHEN (date_format(`input_date`, #md_format) < "01-15") THEN 2
WHEN (date_format(`input_date`, #md_format) < "01-22") THEN 3
-- ... All other cases
ELSE 52
END;
See the docs for the syntax to define a function
Functions will allow you to do operations like:
SELECT week_bucket(payment_date) `week`, SUM(revenue) `revenue`
FROM my_table
WHERE week_bucket(payment_date) > 13
AND week_bucket(payment_date) < 15
GROUP BY `week`;

While loop to add data for pivot

Currently i have a requirement which needs a table to look like this:
Instrument Long Short 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 ....
Fixed 41 41 35 35 35 35 35 35 35 53 25 25
Index 16 16 22 22 22 32 12 12 12 12 12 12
Credits 29 29 41 16 16 16 16 16 16 16 16 16
Short term 12 12 5 5 5 5 5 5 5 5 5 17
My worktable looks like the following:
Instrument Long Short Annual Coupon Maturity Date Instrument ID
Fixed 10 10 10 01/01/2025 1
Index 5 5 10 10/05/2016 2
Credits 15 15 16 25/06/2020 3
Short term 12 12 5 31/10/2022 4
Fixed 13 13 15 31/03/2030 5
Fixed 18 18 10 31/01/2019 6
Credits 14 14 11 31/12/2013 7
Index 11 11 12 31/10/2040 8
..... etc
So basically the long and the short in the pivot should be the sum of each distinct instrument ID. And then for each year i need to take the sum of each Annual Coupon until the maturity date year where the long and the coupon rate are added together.
My thinking was that i had to create a while loop which would populate a table with a record for each year for each instrument until the maturity date, so that i could then pivot using an sql pivot some how. Does this seem feasible? Any other ideas on the best way of doing this, particularly i might need help on the while loop?
The following solution uses a numbers table to unfold ranges in your table, performs some special processing on some of the data columns in the unfolded set, and finally pivots the results:
WITH unfolded AS (
SELECT
t.Instrument,
Long = SUM(z.Long ) OVER (PARTITION BY Instrument),
Short = SUM(z.Short) OVER (PARTITION BY Instrument),
Year = y.Number,
YearValue = t.AnnualCoupon + z.Long + z.Short
FROM YourTable t
CROSS APPLY (SELECT YEAR(t.MaturityDate)) x (Year)
INNER JOIN numbers y ON y.Number BETWEEN YEAR(GETDATE()) AND x.Year
CROSS APPLY (
SELECT
Long = CASE y.Number WHEN x.Year THEN t.Long ELSE 0 END,
Short = CASE y.Number WHEN x.Year THEN t.Short ELSE 0 END
) z (Long, Short)
),
pivoted AS (
SELECT *
FROM unfolded
PIVOT (
SUM(YearValue) FOR Year IN ([2013], [2014], [2015], [2016], [2017], [2018], [2019], [2020],
[2021], [2022], [2023], [2024], [2025], [2026], [2027], [2028], [2029], [2030],
[2031], [2032], [2033], [2034], [2035], [2036], [2037], [2038], [2039], [2040])
) p
)
SELECT *
FROM pivoted
;
It returns results for a static range years. To use it for a dynamically calculated year range, you'll first need to prepare the list of years as a CSV string, something like this:
SET #columnlist = STUFF(
(
SELECT ', [' + CAST(Number) + ']'
FROM numbers
WHERE Number BETWEEN YEAR(GETDATE())
AND (SELECT YEAR(MAX(MaturityDate)) FROM YourTable)
ORDER BY Number
FOR XML PATH ('')
),
1, 2, ''
);
then put it into the dynamic SQL version of the query:
SET #sql = N'
WITH unfolded AS (
...
PIVOT (
SUM(YearValue) FOR Year IN (' + #columnlist + ')
) p
)
SELECT *
FROM pivoted;
';
and execute the result:
EXECUTE(#sql);
You can try this solution at SQL Fiddle.