For loop in a select statement? - sql

Can you use a for loop in a select statement (specifically, a case) in order to define multiple values?
Here's my code:
select case when sysdate in ( declare k DATE:= '2015-01-01',
Begin
FOR i in 1...365 LOOP
k:=sydate +1;
END LOOP;
END;
) then '1' else 'n/a' end FISCAL_YEAR from dual
There may be multiple issues with my syntax..I am trying to say that when today's date is within the year, then a '1' shows up in the FISCAL_YEAR column. This would be a much cleaner way then using a case statement for each day, so any help would be appreciated. Thank you.

You probably just need something like:
select case when to_char(sysdate,'YYYY') = '2015' then 1 else 0 end
from dual
No idea what you're trying to do with that "nested" pl/sql block O.o
Just fill in "the year" in place of 2015 as you need. (ie a variable as you need).
If that's not what you need, please provide a small sample showing input and output expected.

There are easier ways of doing this:
SELECT CASE WHEN TRUNC(sysdate, 'YEAR') = DATE'2015-01-01' THEN '1' ELSE 'n/a' END AS fiscal_year
FROM dual;
or you could use DECODE():
SELECT DECODE(TRUNC(sysdate, 'YEAR', DATE'2015-01-01', '1', 'n/a') AS fiscal_year
FROM dual;
Instead of TRUNC(sysdate, 'YEAR') you could use TO_CHAR(sysdate, 'YYYY') etc.

There are two places you can put your check, depending on which rows you want in your result set. If you want all rows back to a specific date which may be beyond the current FY, use a check in a case in the selection list.
select <whatever>,
case when DateField between date '2015-01-01'
and Add_Months( date '2015-01-01', 12 ) - 1
then 'Y' else 'N' end
as InFY2015
from DataSource;
If you want the result set to contain only rows with dates within the FY, put the check in the where clause.
select <whatever>
from DataSource
where DateField between date '2015-01-01'
and Add_Months( date '2015-01-01', 12 ) - 1;
In the latter query, there is no need for an indicator that says "this row in FY2015" because they all are.

I think previous answer with time interval is the right solution to your problem.
However to answer your question whether there is a way how to loop inside select statement: No, but there is a way how to generate set of increasing numbers, so this code works like your intended pseudocode:
select case when a.c>0 then '1' else 'n/a' end FISCAL_YEAR
from (select count(*) c from (
SELECT ROWNUM+to_date('2015-01-01','yyyy-mm-dd')-1 n
FROM ( SELECT 1
FROM dual
CONNECT BY LEVEL <= 365)) lo
where trunc(sysdate)=lo.n) a

Related

how to convert static to dynamic function/procedure or query

DESIRE O/P LINK WHICH I EXPECT PLEASE CLICK THIS SCREENSHOOT.PNGI created following static query
SELECT '1' AS KPI_ID,
'P2P' AS KPI_DESC,
'22-MAR-17' AS dates,
(SUM (
CASE
WHEN S_DATE BETWEEN ADD_MONTHS ('22-MAR-17', 0) - 13
AND ADD_MONTHS ('22-MAR-17', 0) - 7
THEN
VOLUME
ELSE
0
END))
LAST_WEEK_Volume,
(SUM (
CASE
WHEN S_DATE BETWEEN ADD_MONTHS ('22-MAR-17', 0) - 6
AND ADD_MONTHS ('22-MAR-17', 0)
THEN
VOLUME
ELSE
0
END))
THIS_WEEK_Volume
FROM TABLE
problem statement:
Actually I have two years old data ..
I want calculate last week volume,current week volume day wise for 2 years.
So what should I modify in query so I can get dynamic query.
The current query gives only 1 day calculation.
I want to do per day wise calculation.
According to google I found that after defining paramater start_date and end_date and passing to column it can be possible,but i dont know what is right or wrong ?
Could u help me out please?
You could try something like this:
with dates_table as (
SELECT to_date(:begin_date, 'dd/mm/yyyy') + ROWNUM - 1 c_date
FROM dual
CONNECT BY LEVEL <= to_date(:end_date, 'dd/mm/yyyy') - to_date(:begin_date, 'dd/mm/yyyy') + 1)
SELECT '1' AS KPI_ID,
'P2P' AS KPI_DESC,
c_date AS dates,
(SUM (
CASE
WHEN S_DATE BETWEEN ADD_MONTHS (c_date, 0) - 13
AND ADD_MONTHS (c_date, 0) - 7
THEN
VOLUME
ELSE
0
END))
LAST_WEEK_Volume,
(SUM (
CASE
WHEN S_DATE BETWEEN ADD_MONTHS (c_date, 0) - 6
AND ADD_MONTHS (c_date, 0)
THEN
VOLUME
ELSE
0
END))
THIS_WEEK_Volume
FROM TABLE, dates_table
Here dates_table will contain all the dates between begin_date and end_date (as rows). So we join that table with your 'static' query to get the desired result.
What is "dynamic" in this query? Seems to be '22-MAR-17' (which was Wednesday).
So, if you pass a different date, you'll get a different result. How to pass it? Using a variable. As you tagged SQL Developer, you'd use a :param_name syntax (parameter name precede by a colon), such as
select count(*) from emp where deptno = :par_deptno
If it is a PL/SQL (is it? How?), then what kind of a PL/SQL is it? If anonymous block, you could use the same principle as above (i.e. :param_name). If it is a procedure, you'd rather create a parameter and pass a value to it.

How do I write a split statement in SQL, that divides a record into 2 records if months are different for start and end dates?

I have the following SQL code, and I need to break a row into two rows if the 'COV_PRD_STRT_DT' and 'COV_PRD_END_DT' are different months .
WITH CTE AS
( SELECT COV_PRD_STRT_DT,TO_DATE,COV_PRD_END_DT as MO_END_DT,
case when dateadd (DAY,-DAY(DATEADD(MONTH,1, BF.COV_PRD_STRT_DT)),
DATEADD(MONTH,1, d.from_date))
< To_date THEN DATEADD(DAY,-DAY(DATEADD(MONTH,1, BF.COV_PRD_END_DT)),
DATEADD(MONTH,1, BF.COV_PRD_END_DT))
ELSE To_Date END as MO_END_DT
FROM BILLING_FACT
UNION ALL
SELECT COV_PRD_STRT_DT,To_date,DATEADD(DAY,1,BF.COV_PRD_END_DT) as MO_END_DT, < TO_DATE
THEN DATEADD(DAY,-1,DATEADD(MONTH,1,DATEADD(DAY,1,BF.COV_PRD_END_DT)))
ELSE To_Date END as MO_END_DT
FROM CTE WHERE COV_PRD_END_DT < To_Date
)
select * from CTE order by COV_PRD_STRT_DT,COV_PRD_END_DT
thanks
If start date and end date is not equal on month level, use cov_prd_strt_dt as start date and get its month end date as end date and have the month start date of cov_prd_end_dt as the other row's start date and cov_prd_end_dt as end date to split the dates
SELECT cov_prd_strt_dt strt_dt,
CASE WHEN DATE_PART('Month', cov_prd_strt_dt) <> DATE_PART('Month', cov_prd_end_dt)
THEN LAST_DAY(cov_prd_strt_dt)
ELSE cov_prd_end_dt
END end_dt
FROM billing_fact
UNION
SELECT CASE WHEN DATE_PART('Month', cov_prd_strt_dt) <> DATE_PART('Month', cov_prd_end_dt)
THEN DATE_TRUNC(cov_prd_end_dt)
ELSE cov_prd_strt_dt
END strt_dt,
cov_prd_end_dt end_dt,
FROM billing_fact
Using UNION only will eliminate duplicate records for those that were not processed for split (same month for cov_prd_strt_dt and cov_prd_end_dt).
Nice answers for one or two resulting rows. The general case, a join will do a better job, in performance/relational thinking as well as code readability (I know the last point can be a matter of opinion, but never the less it is important)
First I create a temp table with all the relevant months (but I guess you ought to have a similar table yourself already):
Create temp table months as
Select distinct substr(cov_prd_strt_dt,1,7) yyyy_mm
Union
Select distinct substr(cov_prd_end_dt,1,7) yyyy_mm
Then to the actual join:
Select
*,
substr(cov_prd_start_dt,1,7) start_yyyy_mm,
substr(cov_prd_end_dt,1,7) end_yyyy_mm
From billing_fact f
Join months m
On m.yyyy_mm between f.start_yyyy_mm and f.end_yyyy_mm
I hope this helps

SQL many sysdate in a query

I have a big query with many sysdate, everytime i must check for some sysdate ( +1 +2 +3 etc) i must change in every part and i lost many time, i'd like to create a variable or something on top of my query with unique sysdate who change every sysdate in the query.
this is a little part as example
SELECT DISTINCT(COD)
FROM TABLE_COD
WHERE START_DATE <= trunc(sysdate + 5)
AND END_DATE >= trunc(sysdate + 5)
AND VALUE = 1
AND (COD_REF = ( SELECT to_char(sysdate + 5, 'D') FROM dual) OR COD_REF = 0)
.......continue
If I understand your question correctly, you need something like this.
with sysdate_plus_n as
(select sysdate+1 as sysdate_plus_1 ,-- other variables
sysdate+5 as sysdate_plus_5 from dual)
--This is a temporary table. So if you query
--(select sysdate_plus_5 from sysdate_plus_n), you will get sysdate+5
--You can change your values in this temporary table
SELECT DISTINCT(COD)
FROM TABLE_COD
WHERE START_DATE <= trunc(select sysdate_plus_5 from sysdate_plus_n)
AND END_DATE >= trunc(select sysdate_plus_5 from sysdate_plus_n)
AND VALUE = 1
AND (COD_REF = ( SELECT to_char(select sysdate_plus_5 from sysdate_plus_n, 'D')
FROM dual) OR COD_REF = 0)
.......continue
Now just change values in the temporary table. You dont have to touch the query.
You can join the date:
select ...
from ...
cross join (select trunc(sysdate) + 5 as mydate from dual)
where start_date <= mydate
and end_date >= mydate
and value = 1
and cod_ref in (to_char(mydate, 'D', 'NLS_DATE_LANGUAGE=AMERICAN'), 0)
...
Please see also that I added the NLS_DATE_LANGUAGE to TO_CHAR, because otherwise the output would be session-dependent (i.e. one session may regard Sunday the first day of the week, another Monday).
Use a substitution variable i.e. & and enter the value when prompted. Thus, you will enter the value only once and it will be used simultaneously everywhere in the code.
For example,
SQL> SELECT &dt FROM dual;
Enter value for dt: SYSDATE
old 1: SELECT &dt FROM dual
new 1: SELECT SYSDATE FROM dual
SYSDATE
---------
23-DEC-15
SQL> /
Enter value for dt: SYSDATE +5
old 1: SELECT &dt FROM dual
new 1: SELECT SYSDATE +5 FROM dual
SYSDATE+5
---------
28-DEC-15
AND (COD_REF = ( SELECT to_char(sysdate + 5, 'D') FROM dual) OR COD_REF = 0)
On a side note, you don't have to use a sub-query all the time. SYSDATE is an in-built function, so you can call it directly. No need to select it from dual.
AND (COD_REF = to_char(sysdate + 5, 'D') OR COD_REF = 0)
If you want multiple days at a time, how about writing one query for the purpose? Something like this:
WITH params AS (
SELECT 5 as val FROM DUAL UNION ALL
SELECT 6 as val FROM DUAL
)
SELECT DISTINCT v.val, COD
FROM params CROSS JOIN
TABLE_COD c
WHERE START_DATE <= trunc(sysdate + params.val) AND END_DATE >= trunc(sysdate + params.val) AND
VALUE = 1 AND
(COD_REF = (to_char(sysdate + params.val, 'D') OR COD_REF = 0);
With this structure, you can add as many values as you want to the query (including only one).
As I understand, the value of SYSDATE is evaluated only once before the query is actually ran. In other words, the value of SYSDATE will stay constant throughout your complete query. Knowing this detail may greatly help or solve your question.
I do think the best solution in your case would be to create a user function to be called as many times as you want from within your select statement. Then, when you need any changes, just update the function instead.
I don't think there's a way to create a variable at the beginning of your query in order to be reused through out a single statement. You can however, use the WITH clause in order to avoid repeating embedded queries and improve performance and ease of reading. This is similar to what you need, but wanted to mention it as it might be a solution too depending on your needs.
Check out this link for more information on Oracle Functions:
https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_5009.htm
I hope this helps!
WITH FIX_SYSDATE AS
(SELECT TRUNC(SYSDATE) AS FIXED_SYSDATE
FROM DUAL)
SELECT DISTINCT(COD)
FROM TABLE_COD
CROSS JOIN FIX_SYSDATE FS
WHERE START_DATE <= FS.FIXED_SYSDATE + 5
AND END_DATE >= FS.FIXED_SYSDATE + 5
AND VALUE = 1
AND (COD_REF = (SELECT TO_CHAR(FS.FIXED_SYSDATE + 5, 'D')
FROM DUAL)
OR COD_REF = 0)
.......continue
Fix TRUNC(SYSDATE) or simply SYSDATE (I don't know the remaining of your query, since there is "... continue" there) and then use it as many times as required on your main query.

Counting business days between two dates for each row in a table

I'm trying to implement this askTom solution to count the business days between two date fields in a table.
select count(*)
from ( select rownum rnum
from all_objects
where rownum <= to_date('&1') - to_date('&2')+1 )
where to_char( to_date('&2')+rnum-1, 'DY' ) not in ( 'SAT', 'SUN' )
I don't know how I can pass values to toms code or how to do a work around so that each time the select executes with a different set of dates and that way obtaining a similar output :
rowid | bussiness_days
I guess this could be implemented with a PL/SQL block but I'd prefer to keep it down to a query if possible. Could it be possible to pass values using &1 parameters from a select above toms one?
This is not like the original askTom, but if you're using 11gR2 you can use a Recursive CTE:
with rcte(a,b, i, is_wd) as (
select from_dt , to_dt , id, case when (to_char(from_dt, 'DY') in ('SAT','SUN')) then 0 else 1 end from t
union all
select decode(a, null, a,a+1), b, i, case when (to_char(a, 'DY') in ('SAT','SUN')) then 0 else 1 end
from rcte
where a+1 <= b
)
select i id, sum(is_wd)
from rcte
group by i
where t is a table containing "from_dates" and "to_dates"
Here is a sqlfiddle demo
Try this one:
SELECT COUNT(*)
FROM ( SELECT ROWNUM rnum
FROM all_objects
WHERE ROWNUM <= TO_DATE('2014-02-07','yyyy-mm-dd') - TO_DATE('2014-02-01','yyyy-mm-dd')+1 )
WHERE TO_CHAR( TO_DATE('2014-02-01','yyyy-mm-dd')+rnum-1, 'DY' ) NOT IN ( 'SAT', 'SUN' )
However, it has at least two issues:
It presumes the session NLS_DATE_LANGUAGE is set to English
The query gives wrong result if number of rows in ALL_OBJECTS is smaller than the amount of days between your ranges.
The version is less error-prone and faster:
WITH t AS
(SELECT TO_DATE('2014-02-01','yyyy-mm-dd')+LEVEL-1 the_date
FROM dual
CONNECT BY TO_DATE('2014-02-01','yyyy-mm-dd')+LEVEL-1 <= TO_DATE('2014-02-07','yyyy-mm-dd'))
SELECT COUNT(*)
FROM t
WHERE TO_CHAR(the_date, 'DY', 'NLS_DATE_LANGUAGE = AMERICAN') NOT IN ( 'SAT', 'SUN' )
This is another situation where a calendar table comes in handy. A calendar table has a row for every date.
If Saturday and Sunday are not business days, it's quite likely that holidays are not either. A field in the calendar table to indicate holidays makes it a lot easier to exclude them from your business days calculation.

oracle case or decode? - how to handle no recs

How can I make sure in this case statement that if I get now rows selected that I set that my result=1?
I want a value of 1 if I have no records or a count of 0. I would also want a null date to be sysdate by default.
Here is what I have so far. It works with (sysdate-1), but when I tested it with (sysdate-0) or =sysdate (today's date), I
was getting no records. So I want to be able to handle null values too.
How can I change this query to do that? or would I use something like DECODE?
select
to_date(year||'/'||month||'/'||day, 'YYYY/MM/DD') as mydt,
case when count(*) = 0 then 1 else 0 end result
from Table1
where
to_date(year||'/'||month||'/'||day, 'YYYY/MM/DD') >= trunc(sysdate)-1
GROUP BY
to_date(year||'/'||month||'/'||day, 'YYYY/MM/DD')
Here is my table desc. Perhaps, I should go off of timestamp instead?
There is also a value of counts which may help change this query or not. So what i'm looking for is coming up
with a value of 1 or 0 with a timestamp if that's possible.
SQL> desc Table1
Name Null? Type
----------------------------------------- -------- ----------------------------
YEAR NUMBER
QUARTER NUMBER
MONTH NUMBER
DAY NUMBER
HOUR NUMBER
TIMESTAMP NUMBER
CNT_N NUMBER
ACTN_N NUMBER
ADDR_C VARCHAR2(255)
You will never get a count(*) of zero with a group by. If there are no rows, you won't get any groups and if there are rows then any group you get will show the number of rows.
If you have 3 rows for January 1st and another 3 for January 3rd, you will get :
2011/01/01 3
2011/01/03 3
You won't get a row for January 2nd. Are you wanting to generate that row ? Is there a maximum number of rows that you want returned ?
I think you can do modify your query like below to assign the sysdate to null values
select
nvl(to_date(year||'/'||month||'/'||day, 'YYYY/MM/DD') ,sysdate) as mydt
from Table1
where
nvl(to_date(year||'/'||month||'/'||day, 'YYYY/MM/DD') ,sysdate)
>= trunc(sysdate)-1
GROUP BY nvl(to_date(year||'/'||month||'/'||day, 'YYYY/MM/DD') ,sysdate)
but your second requirement is not very much understood.... please clarify what exectly you want with count(*)
if you want to assign 1 for null date count then you can add following line in select statment
case
when nvl(to_date(year||'/'||month||'/'||day, 'YYYY/MM/DD') ,sysdate) = sysdate then
1
else
count(*)-- or 0 ???!!!!!
It looks like you need to generate the date(s) and then see how may records match each one. Assuming you don't have any data in the future you can do something like:
select trunc(sysdate) + level - (:range + 1) as dt
from dual
connect by level <= (:range + 1);
which with :range set to 1 gives two dates:
DT
---------
19-JUL-11
20-JUL-11
You can then have outer join to that list of dates:
with tmp_dt as (
select trunc(sysdate) + level - (:range + 1) as dt
from dual
connect by level <= (:range + 1)
)
select td.dt as mydt,
case when count(t.year) = 0 then 1 else 0 end as result
from tmp_dt td
left join table1 t on t.year = extract(year from td.dt)
and t.month = extract(month from td.dt)
and t.day = extract(day from td.dt)
group by td.dt
order by td.dt;
If I only have any data in the table for 19-Jul-11, I get:
MYDT RESULT
--------- ----------
19-JUL-11 0
20-JUL-11 1
If you do have data in the future this won't show it; range is how many days to look in the past. If you know there's a limit of, say, seven days you can use connect by level <= (:range + 1) + 7 or have a second variable, but it rather depends on what you want to see.
I've swapped the join around a bit to avoid doing a date conversion for every row in the table, extracting the relevant part of the generated date instead. I hope you have a reason for storing the date components in separate fields rather than as a date.
If you're only looking for data from today, just change the date generator:
with tmp_dt as (select trunc(sysdate) as dt from dual)
select td.dt as mydt,
case when count(t.year) = 0 then 1 else 0 end result
from tmp_dt td
left join table1 t on t.year = extract(year from td.dt)
and t.month = extract(month from td.dt)
and t.day = extract(day from td.dt)
group by td.dt;
If you always want yesterday, it would be select trunc(sysdate) - 1 as dt from dual, etc.
May be you can encapsulate your whole query within the NVL function
like this
SELECT NVL(TO_CHAR((select to_date(year || '/' || month || '/' || day,
'YYYY/MM/DD') as mydt
from Table1
where to_date(year || '/' || month || '/' || day,
'YYYY/MM/DD') >= trunc(sysdate) - 1
GROUP BY to_date(year || '/' || month || '/' || day,
'YYYY/MM/DD')),
'DD/MM/YYYY HH:MI:SS AM'),
'1')
FROM DUAL
I have removed the case statement from the query
Instead , what this query will do is, in case your query does return any value, it will do so without interference. However, in case the query returns NOTHING, then the NVL function will take over and return 1
I have used the TO_CHAR function to maintain the datatype of both the arguments in the NVL function. Both the date returned and the value for NVL are character based
Hope it helps