PL/SQL newbie: Functions with SELECT and SYSDATE - sql

I'm new to SQL and was hoping for some help turning this into a function:
SELECT
SYSDATE "Today's Date",
NEXT_DAY(trunc(SYSDATE, 'MONTH')-1, 'Tuesday')"First Tuesday this Month",
NEXT_DAY (LAST_DAY(SYSDATE)+1,'TUESDAY') "First Tuesday of Next Month"
FROM DUAL;
Here's my attempt at turning the above into a function (as you can see it didn't go so hot)...
CREATE OR REPLACE FUNCTION first_tuesday
RETURN DATE
AS
date1 DATE;
date2 DATE;
date3 DATE;
BEGIN
date1 := SELECT SYSDATE;
date2 := SELECT NEXT_DAY(trunc(SYSDATE, 'MONTH')-1, 'Tuesday');
date3 := SELECT NEXT_DAY (LAST_DAY(SYSDATE)+1,'TUESDAY');
RETURN 'todays date:' || date1,
'first tuesday this month:' || date2,
'first tuesday next month:' || date3;
END;
/
SELECT first_tuesday FROM DUAL;
What am I doing wrong?

You're really trying to return three values there. I'd suggest breaking it out into separate functions, but leave "today's date" just as a call to sysdate.
You don't need to issue a SELECT query to populate a variable, and in this case you can just return an expression.
CREATE OR REPLACE FUNCTION first_tuesday
RETURN DATE
AS
BEGIN
RETURN NEXT_DAY(trunc(SYSDATE, 'MONTH')-1, 'Tuesday')
END;
/
Something to look out for here is that within the execution of a SQL statement, SYSDATE always has the same value, but that's not the case in PL/SQL. So when sysdate is evaluated in PL/SQL that is called from a SQL statement, you can get a different value in each context. So if you call:
select sysdate col1,
some_function col2
from dual;
... and during it's lengthy execution some_function calculates sysdate, it could be using a slightly different (later) value than the parent SQL statement.
The way to avoid this is to pass sysdate into the function:
CREATE OR REPLACE FUNCTION first_tuesday (sysdate_in date)
RETURN DATE
AS
BEGIN
RETURN NEXT_DAY(trunc(sysdate_in, 'MONTH')-1, 'Tuesday')
END;
/
So then you might ...
Select ...
From invoices
Where invoice_date < first_tuesday(sysdate)
That's a trivial example just to demonstrate the principle, and it really only matters for functions called from SQL.
To be honest, most practitioners in your situation would probably skip the function and just:
Select ...
From invoices
Where invoice_date < NEXT_DAY(trunc(sysdate, 'MONTH')-1, 'Tuesday')
Edit: by the way, you probably want to use:
NEXT_DAY (LAST_DAY(SYSDATE),'TUESDAY')
... not ...
NEXT_DAY (LAST_DAY(SYSDATE)+1,'TUESDAY')

Related

Error: an INTO clause is expected in this SELECT statement

I'm going to write a if statement within that if condition pass need to run first select statement else second one. But it showing an INTO clause is expected in this SELECT statement
declare
x integer;
begin
select to_char(sysdate,'hh') into x from dual;
if x > 12 then
select sysdate from dual;
else
select sysdate+1 from dual;
end if;
end;
You are running all three queries in a PL/SQL context (within an anonymous PL/SQL block, between the begin and end), so they all need to follow the PL/SQL rules.
That means they all need to be selected into something (or treated as cursors), so you need a date variable to select into, and then you need to do something with that variable:
declare
x integer;
d date;
begin
select to_number(to_char(sysdate, 'hh24')) into x from dual;
if x > 12 then
select sysdate into d from dual;
else
select sysdate+1 into d from dual;
end if;
-- do something with the variable
dbms_output.put_line(to_char(d, 'YYYY-MM-DD HH24:MI:SS'));
end;
/
Notice that I've also changed the first query to use the hh24 format element, because hh gives the 12-hour-clock time, which can never be more than 12.
You don't really need the first query, you can just do:
declare
d date;
begin
if to_number(to_char(sysdate, 'hh24')) > 12 then
select sysdate into d from dual;
...
But it looks like you're only using PL/SQL so you can use the if/then/else construct, which isn't available in plain SQL. That does have case though, so as #jarlh said you can do:
select
case
when to_number(to_char(sysdate,'hh24')) > 12
then sysdate
else sysdate + 1
end as result
from dual;
or
select
case
when extract(hour from systimestamp) > 12
...
You are also retaining the current system time, even if you change the date to tomorrow; you might want to set the time to midnight, which you can do with trunc(sysdate). It's unclear which you want though.
The logic also looks odd, but again it's unclear what you intended; 00-12 is modified to tomorrow and 13-23 kept as today, which might be backwards. If you want any time after (and including?) midday to be treated as tomorrow then you should be adding a day if the hour is 12 or above, so 00-11 is kept as today and 12-23 is modified to tomorrow. If that is what you meant then you can do:
select case
when extract(hour from systimestamp) >= 12
then trunc(sysdate) + 1
else trunc(sysdate)
end as result
from dual;
Or even more simply, assuming you don't want to preserve the actual time this is run, you can do:
select trunc(sysdate + 12/24) as result
from dual;
fiddle

Create date from year, month and day

Does Oracle have a builtin function to create a date from its individual components (year, month and day) that just returns null on missing data?
I'm aware of TO_DATE() but I need to compose a string first and neither the || operator nor the CONCAT() function make it easy to handle missing data:
-- my_year NUMBER(4,0) NULL
SELECT TO_DATE(my_year || '-01-01', 'YYYY-MM-DD') AS my_date
FROM my_table;
Whenever my_year is NULL we end up with TO_DATE('-01-01', 'YYYY-MM-DD') and:
ORA-01841: (full) year must be between -4713 and +9999, and not be 0
For your example, you can use case:
select (case when my_year is not null and my_year <> 0 and
my_year between -4713 and 9999
then TO_DATE(my_year || '-01-01', 'YYYY-MM-DD')
end)
Unfortunately, Oracle does not have a method of doing the conversion, if possible, and otherwise returning NULL. SQL Server recently introduced try_convert() for this purpose.
One option is to write your own function with an exception handler for the failed conversion. The exception handler would simply return NULL for a bad format.
You can't use year zero with to_date('0000-01-01', 'YYYY-MM-DD'), but oddly you can with a date literal date '0000-01-01'. On its own that becomes year -1, but you can use it calculations; and you can add an interval too which can be based on your numeric value, e.g.:
SELECT DATE '0000-01-01' + NUMTOYMINTERVAL(my_year, 'YEAR') AS my_date
FROM my_table;
The numtoyminterval function returns null if the argument is null, and adding that to a fixed date also gives you null:
alter session set nls_date_format = 'SYYYY-MM-DD';
select date '0000-01-01' + numtoyminterval(null, 'YEAR') from dual;
DATE'0000-01-01'+NUMTOYMINTERVAL(NULL,'YEAR')
---------------------------------------------
select date '0000-01-01' + numtoyminterval(2015, 'YEAR') from dual;
DATE'0000-01-01'+NUMTOYMINTERVAL(2015,'YEAR')
---------------------------------------------
2015-01-01
select date '0000-01-01' + numtoyminterval(9999, 'YEAR') from dual;
DATE'0000-01-01'+NUMTOYMINTERVAL(9999,'YEAR')
---------------------------------------------
9999-01-01
select date '0000-01-01' + numtoyminterval(-4712, 'YEAR') from dual;
DATE'0000-01-01'+NUMTOYMINTERVAL(-4712,'YEAR')
----------------------------------------------
-4712-01-01
It isn't foolproof; it will still error if you try to go before -4713:
select date '0000-01-01' + numtoyminterval(-4713, 'YEAR') from dual;
SQL Error: ORA-01841: (full) year must be between -4713 and +9999, and not be 0
...
Though you can avoid that with a check constraint on the column. And because of the silent translation of year 0 to -1, you get the same answer if your my_year value is 0 or -1:
select date '0000-01-01' + numtoyminterval(0, 'YEAR') from dual;
DATE'0000-01-01'+NUMTOYMINTERVAL(0,'YEAR')
------------------------------------------
-0001-01-01
select date '0000-01-01' + numtoyminterval(-1, 'YEAR') from dual;
DATE'0000-01-01'+NUMTOYMINTERVAL(-1,'YEAR')
-------------------------------------------
-0001-01-01
Gordon Linoff's case approach is more robust, but this might work if you're only really dealing with 'sane' positive years. And if not, it's mildly interesting...
I've eventually composed a user-defined function to encapsulate the logic. It returns a date from its individual components or NULL if the date is not valid:
CREATE OR REPLACE FUNCTION TRY_TO_DATE (
V_YEAR IN NUMBER,
V_MONTH IN NUMBER,
V_DAY IN NUMBER
) RETURN DATE DETERMINISTIC IS
BEGIN
RETURN TO_DATE(LPAD(V_YEAR, 4, '0') || LPAD(V_MONTH, 2, '0') || LPAD(V_DAY, 2, '0'), 'YYYY-MM-DD');
EXCEPTION
WHEN OTHERS THEN RETURN NULL;
END TRY_TO_DATE;
/
Fiddle
where my_year > 0
This will just return rows where it's possible to create a date. Ignoring rows with null or with a value of 0. If you have some values not in the range -4713 and +9999, you should of course exclude those in the where clause too.

Select doesn't return expected result with Date type in Oracle DB

Table my_table is:
CREATE TABLE MY_TABLE(
ID NUMBER NOT NULL,
MY_DATE DATE NOT NULL);
Typing the following query:
select sysdate from dual
the result is:
10-MAG-12 21:22:32
note that mag = may.
Now, if I type this query:
select *
from my_table
where my_date <= sysdate
the result is:
9918 10-MAG-12 20:00:00
9915 10-MAG-12 21:00:00
9952 10-MAG-12 22:00:00
9951 10-MAG-12 23:00:00
Note that in my_table I have only these 4 records. Why I see all the records and not the first and second record only? Thanks.
I use Oracle SQL Developer.
Edit: please note that when I insert a record with PL/SQL I type something like:
nCount NUMBER;
myDate DATE;
stringDate VARCHAR2(255);
BEGIN
nCount := 0;
stringDate := substr(to_char(trunc(sysdate)),0,9);
myDate := to_date(stringDate || ' 20:00:00','dd-mm-yyyy hh24:mi:ss');
for t in (a cursor) loop
insert into MY_TABLE(ID,MY_DATE)
values (9918,myDate+(nCount/1440));
nCount := nCount + 60;
end loop;
END;
I suspect that the data being stored in your table does not have a year of 2012. It probably has a year of 0012 (two thousand years ago).
What do you see when you run the query
SELECT id, to_char( my_date, 'dd-mm-yyyy hh24:mi:ss' )
FROM my_table
I expect that the year will be 0012 rather than 2012. The reason for that is that the code you're using to insert the data is incorrectly converting a date to a string without using an explicit format mask then converts the string back to a date using an explicit format mask that happens not to match the session's NLS_DATE_FORMAT. In general, if you ever find yourself converting a date to a string and back to a date, you're probably doing something wrong. If you change your code to simply do date manipulation, it will be more efficient, more robust, and less error-prone.
DECLARE
nCount NUMBER;
myDate DATE;
BEGIN
nCount := 0;
myDate := trunc(sysdate) + interval '20' hour;
for t in (a cursor) loop
insert into MY_TABLE(ID,MY_DATE)
values (9918,myDate+(nCount/1440));
nCount := nCount + 60;
end loop;
END;
Walking through why the original code goes wrong
stringDate := substr(to_char(trunc(sysdate)),0,9);
This takes SYSDATE and truncates it to midnight on the current day. So far, so good. Then, it calls TO_CHAR without an explicit format mask. This causes Oracle to use the session's NLS_DATE_FORMAT, meaning that different users with different settings will get different results. If your session's NLS_DATE_FORMAT happens to be 'dd-mon-rr hh24:mi:ss', which I'm guessing based on the query results you posted, that will mean that the string has a 2-digit year. Your SUBSTR appears to assume that the output has just a two-digit year (if you have a different NLS_DATE_FORMAT, your SUBSTR will generate different bugs such as potentially cutting off the 12 from a year of 2012 leaving a year of just 20).
myDate := to_date(stringDate || ' 20:00:00','dd-mm-yyyy hh24:mi:ss');
Assuming stringDate is something like 10-MAG-12, this next line generates a string 10-MAG-12 20:00:00 and then tries to convert it to a date using the format mask dd-mm-yyyy hh24:mi:ss. This assumes that the string has a 4-digit year so when it only finds 2-digits, it assumes that you meant the year 12, not the year 2012.

Oracle equivalent to SQL Server DATEPART

We need to get the HOUR out of a DATETIME column (expecting values from 0 to 23 to be returned).
Is there an Oracle equivalent of the SQL Server DATEPART function?
An alternative is the EXTRACT function which is an ANSI standard and also works on other DBMS, but it also requires the use of current_timestamp (also ANSI) instead of sysdate
SELECT extract(hour from current_timestamp)
FROM dual
SELECT to_number(to_char(sysdate, 'HH24')) FROM DUAL
I think this is what you are looking for
select to_char(current_timestamp,'HH24') from dual;
for additional ways of using DATEPART kind of function in oracle
DATEPART:-Weekday
--This would give the day for the week for a reference start day
DECLARE
Start_day date:=to_date('30-03-2012','DD-MM-YYYY');
check_date DATE:=SYSDATE;
Day_of_week NUMBER;
i NUMBER;
BEGIN
i:=to_char(Start_day,'D');
day_of_week:=mod((to_char(check_date,'D') -(i-1)+7),7);
if day_of_week=0
THEN
day_of_week:=7;
end if;
dbms_output.put_line(day_of_week);
END;
if you want to get the month name of a date
select to_char( 'month', sysdate())

Oracle equivalent to SQL Server/Sybase DateDiff

We are now using NHibernate to connect to different database base on where our software is installed. So I am porting many SQL Procedures to Oracle.
SQL Server has a nice function called DateDiff which takes a date part, startdate and enddate.
Date parts examples are day, week, month, year, etc. . .
What is the Oracle equivalent?
I have not found one do I have to create my own version of it?
(update by Mark Harrison) there are several nice answers that explain Oracle date arithmetic. If you need an Oracle datediff() see Einstein's answer. (I need this to keep spme SQL scripts compatible between Sybase and Oracle.) Note that this question applies equally to Sybase.
I stole most of this from an old tom article a few years ago, fixed some bugs from the article and cleaned it up. The demarcation lines for datediff are calculated differently between oracle and MSSQL so you have to be careful with some examples floating around out there that don't properly account for MSSQL/Sybase style boundaries which do not provide fractional results.
With the following you should be able to use MSSQL syntax and get the same results as MSSQL such as SELECT DATEDIFF(dd,getdate(),DATEADD(dd,5,getdate())) FROM DUAL;
I claim only that it works - not that its effecient or the best way to do it. I'm not an Oracle person :) And you might want to think twice about using my function macros to workaround needing quotes around dd,mm,hh,mi..etc.
(update by Mark Harrison) added dy function as alias for dd.
CREATE OR REPLACE FUNCTION GetDate
RETURN date IS today date;
BEGIN
RETURN(sysdate);
END;
/
CREATE OR REPLACE FUNCTION mm RETURN VARCHAR2 IS BEGIN RETURN('mm'); END;
/
CREATE OR REPLACE FUNCTION yy RETURN VARCHAR2 IS BEGIN RETURN('yyyy'); END;
/
CREATE OR REPLACE FUNCTION dd RETURN VARCHAR2 IS BEGIN RETURN('dd'); END;
/
CREATE OR REPLACE FUNCTION dy RETURN VARCHAR2 IS BEGIN RETURN('dd'); END;
/
CREATE OR REPLACE FUNCTION hh RETURN VARCHAR2 IS BEGIN RETURN('hh'); END;
/
CREATE OR REPLACE FUNCTION mi RETURN VARCHAR2 IS BEGIN RETURN('mi'); END;
/
CREATE OR REPLACE FUNCTION ss RETURN VARCHAR2 IS BEGIN RETURN('ss'); END;
/
CREATE OR REPLACE Function DateAdd(date_type IN varchar2, offset IN integer, date_in IN date )
RETURN date IS date_returned date;
BEGIN
date_returned := CASE date_type
WHEN 'mm' THEN add_months(date_in,TRUNC(offset))
WHEN 'yyyy' THEN add_months(date_in,TRUNC(offset) * 12)
WHEN 'dd' THEN date_in + TRUNC(offset)
WHEN 'hh' THEN date_in + (TRUNC(offset) / 24)
WHEN 'mi' THEN date_in + (TRUNC(offset) /24/60)
WHEN 'ss' THEN date_in + (TRUNC(offset) /24/60/60)
END;
RETURN(date_returned);
END;
/
CREATE OR REPLACE Function DateDiff( return_type IN varchar2, date_1 IN date, date_2 IN date)
RETURN integer IS number_return integer;
BEGIN
number_return := CASE return_type
WHEN 'mm' THEN ROUND(MONTHS_BETWEEN(TRUNC(date_2,'MM'),TRUNC(date_1, 'MM')))
WHEN 'yyyy' THEN ROUND(MONTHS_BETWEEN(TRUNC(date_2,'YYYY'), TRUNC(date_1, 'YYYY')))/12
WHEN 'dd' THEN ROUND((TRUNC(date_2,'DD') - TRUNC(date_1, 'DD')))
WHEN 'hh' THEN (TRUNC(date_2,'HH') - TRUNC(date_1,'HH')) * 24
WHEN 'mi' THEN (TRUNC(date_2,'MI') - TRUNC(date_1,'MI')) * 24 * 60
WHEN 'ss' THEN (date_2 - date_1) * 24 * 60 * 60
END;
RETURN(number_return);
END;
/
JohnLavoie - you don't need that. DATE in Oracle is actually a date and time data type. The only difference between DATE and TIMESTAMP is that DATE resolves down to the second but TIMESTAMP resolves down to the micro second. Therefore the Ask Tom article is perfectly valid for TIMESTAMP columns as well.
Tom's article is very old. It only discusses the DATE type. If you use TIMESTAMP types then date arithmetic is built into PL/SQL.
http://www.akadia.com/services/ora_date_time.html
DECLARE
ts_a timestamp;
ts_b timestamp;
diff interval day to second;
BEGIN
ts_a := systimestamp;
ts_b := systimestamp-1/24;
diff := ts_a - ts_b;
dbms_output.put_line(diff);
END;
+00 01:00:00.462000
or
DECLARE
ts_b timestamp;
ts_a timestamp;
date_part interval day to second;
BEGIN
ts_a := systimestamp;
date_part := to_dsinterval('0 01:23:45.678');
ts_b := ts_a + date_part;
dbms_output.put_line(ts_b);
END;
04-SEP-08 05.00.38.108000 PM
YOU Could write a function in oracle for this
function datediff( p_what in varchar2, p_d1 in date, p_d2 in date) return number as l_result number;
BEGIN
select (p_d2-p_d1) *
decode( upper(p_what), 'SS', 24*60*60, 'MI', 24*60, 'HH', 24, NULL )
into l_result from dual;
return l_result;
END;
and use it like :
DATEDIFF('YYYY-MM-DD', SYSTIMESTAMP, SYSTIMESTAMP)