Performance of using cursor PLSQL - performance

I have to get a count using a cursor.
the basic cursor is
get *
from table
In here there are three types of data I want to get the count of.
sysdate < start_date
sysdate > startdate and sysdate < enddate
sysdate > enddate
I have 2 ways of doing this.
Loop the above cursor and use a if condition to get the counts.
Create 3 separate cursors with the conditions and directly get the count
As I have a lot of data what way would be good in perspective of performance ?

Don't use a cursor at all.
Use conditional aggregation. That's almost surely faster than any cursor based approach.
SELECT count(CASE
WHEN sysdate < start_date THEN
1
END) count1,
count(CASE
WHEN sysdate > startdate
AND sysdate < enddate THEN
1
END) count2,
count(CASE
WHEN sysdate > enddate THEN
1
END) count3
FROM elbat;

Related

encountered the symbol FROM when expecting one of the following pl sql

I am trying to extract year from datefield but when I use extract (year from
datefield) I get this error
Encountered the symbol FROM when expecting one of the following pl sql
cursor o1 is
select substr(tarifa,1,2), count(*)
from pol p, uvod u, doppov d
where extract(year FROM datum_dop) = EXTRACT(YEAR FROM sysdate)
and izdavanje >='1-jul-13'
and p.orgjed = u.sorgz (+)
and DATUM_PREKIDA is not null
and p.polica=d.polica and d.pov_dopl='P'
and d.status='F'
and cisti_ao(p.polica)!=0
group by substr(tarifa,1,2);
Where did I made mistake ?
Ah, this is Forms, probably 6i.
Its engine doesn't know extract function. Change that line to
where to_char(datum_dop, 'yyyy') = to_char(sysdate, 'yyyy')
This would, though, make index on datum_dop column (if it exists) unusable and force Oracle to convert dates to strings, so you'd rather try with
where datum_dop >= trunc(sysdate, 'yyyy')
and datum_dop < add_months(trunc(sysdate, 'yyyy'), 12)
Other than that:
count(*) should have an alias (if you plan to use it), e.g. count(*) as broj_tarifa
if izdavanje is date, don't compare it to a string ('1-jul-13') but date, e.g. izdavanje >= to_date('01.07.2013', 'dd.mm.yyyy')
use table aliases for all columns

Putting values into a collection for different date ranges

I am writing a PL/SQL procedure which gives the count of a query based on date range values. I want to get the date range dynamically and I have written a cursor for that.
I am using a collection and getting the counts of each month, the problem I am facing is that collection is populated with the count of the last month alone. I want to get the count of all months. Can anyone help?
This is the procedure I have written:
create or replace
Procedure Sample As
Cursor C1 Is
With T As (
select to_date('01-JAN-17') start_date,
Last_Day(Add_Months(Sysdate,-1)) end_date from dual
)
Select To_Char(Add_Months(Trunc(Start_Date,'mm'),Level - 1),'DD-MON-YY') St_Date,
to_char(add_months(trunc(start_date,'mm'),level),'DD-MON-YY') ed_date
From T
Connect By Trunc(End_Date,'mm') >= Add_Months(Trunc(Start_Date,'mm'),Level - 1);
Type T_count_Group_Id Is Table Of number;
V_count_Group_Id T_count_Group_Id;
Begin
For I In C1
Loop
Select Count(Distinct c1) bulk collect Into V_Count_Group_Id From T1
Where C2 Between I.St_Date And I.Ed_Date;
End Loop;
For J In V_Count_Group_Id.First..V_Count_Group_Id.Last
Loop
Dbms_Output.Put_Line(V_Count_Group_Id(J));
end loop;
END SAMPLE;
Your bulk collect query is replacing the contents of the collection each time around the loop; it doesn't append to the collection (if that's what you expected). So after your loop you are only seeing the result of the last bulk collect, which is the latest month from your cursor.
You're also apparently comparing dates as string, which isn't a good idea (unless c2 is stored as a string - which is even worse). And as between is inclusive, you risk including data for the first day of each month in two counts, if the stored time portion is midnight. It's safer to use equality checks for date ranges.
You don't need to use a cursor to get the dates and then individual queries inside that cursor, you can just join your current cursor query to the target table - using an outer join to allow for months with no matching data. Your cursor seems to be looking for all months in the current year, up to the start of the current year, so that could perhaps be simplified to:
with t as (
select add_months(trunc(sysdate, 'YYYY'), level - 1) as st_date,
add_months(trunc(sysdate, 'YYYY'), level) as ed_date
from dual
connect by level < extract(month from sysdate)
)
select t.st_date, t.ed_date, count(distinct t1.c1)
from t
left join t1 on t1.c2 >= t.st_date and t1.c2 < t.ed_date
group by t.st_date, t.ed_date
order by t.st_date;
You can use that to populate your collection:
declare
type t_count_group_id is table of number;
v_count_group_id t_count_group_id;
begin
with t as (
select add_months(trunc(sysdate, 'YYYY'), level - 1) as st_date,
add_months(trunc(sysdate, 'YYYY'), level) as ed_date
from dual
connect by level < extract(month from sysdate)
)
select count(distinct t1.c1)
bulk collect into v_count_group_id
from t
left join t1 on t1.c2 >= t.st_date and t1.c2 < t.ed_date
group by t.st_date, t.ed_date
order by t.st_date;
for j in v_count_group_id.first..v_count_group_id.last
loop
dbms_output.put_line(v_count_group_id(j));
end loop;
end;
/
although as it only stores/shows the counts, without saying which month they belong to, that might not ultimately be what you really need. As the counts are ordered, you at least know that the first element in the collection represents January, i suppose.

Datepart function in oracle

I have some sample records in Oracle 12
Date_Time Item
10/1/2012 12:05:00 AM 3
12/3/2012 06:00:00 AM 2
11/8/2012 14:05:05 PM 10
12/9/2012 16:00:59 PM 5
I like to aggregate the Item field based on military time or in three different times: 00:00:00AM to 05:59:00AM, 06:00:00AM to 15:59:00PM, and 16:00:00PM to 23:59:00PM. I was able to use the Datepart function in SQL to do this. I was wondering what function in Oracle 12 that allows me to count the Item between these three different times.
My desired output would be:
Date_Time Count
00:00:00AM to 05:59:00AM = 3
06:00:00AM to 15:59:00PM = 12
16:00:00PM to 23:59:00PM = 5
In oracle, date datatype contains date+time ,so you just need just use group by
SELECT Date_Time, COUNT(*) item FROM YOUR_TABLES
GROUP BY Date_Time;
NEW Answer:
SELECT TO_CHAR(DATE_TIME,'HH24') time, count(*) FROM YOUR_TABLE
WHERE TO_CHAR(DATE_TIME,'HH24') >= '00'
AND TO_CHAR(DATE_TIME,'HH24') < '06'
GROUP BY TO_CHAR(DATE_TIME,'HH24')
UNION ALL
SELECT TO_CHAR(DATE_TIME,'HH24') time, count(*) FROM YOUR_TABLE
WHERE TO_CHAR(DATE_TIME,'HH24') >= '06'
AND TO_CHAR(DATE_TIME,'HH24') < '16'
GROUP BY TO_CHAR(DATE_TIME,'HH24')
UNION ALL
SELECT TO_CHAR(DATE_TIME,'HH24') time, count(*) FROM YOUR_TABLE
WHERE TO_CHAR(DATE_TIME,'HH24') >= '16'
AND TO_CHAR(DATE_TIME,'HH24') < '00'
GROUP BY TO_CHAR(DATE_TIME,'HH24');
and if your table is huge :
first :partition it
second: create local functional index on TO_CHAR(DATE_TIME,'HH24:MI:SS')
Assuming that date_time column is datatype DATE, we can use the TO_CHAR function to extract a two character representation... in the range 00 to 23.
(The selected answer demonstrates this approach to extracting the "hour" from an Oracle DATE.)
Assuming that we want every non-null time value to fall into one of three time ranges... that is, if we don't want any of time values to be omitted because of a crack/gap in between the ranges, and we don't want any overlap in the ranges...
We can use a simple "less than" tests in a CASE expression.
Consider a time close to a boundary: '05:59:33'. That's after 05:59:00 but before 06:00:00. If we want that included in the first range, we can just test for hour < '06'.
If was grouping the rows into three ranges, and I wanted a total of the item column, I'd do something like this:
SELECT CASE
WHEN TO_CHAR( t.date_time ,'HH24') < '06' THEN '00:00:00 to 05:59:59'
WHEN TO_CHAR( t.date_time ,'HH24') < '16' THEN '06:00:00 to 15:59:59'
WHEN TO_CHAR( t.date_time ,'HH24') < '24' THEN '16:00:00 to 23:59:59'
END AS time_range
, SUM(t.item)
FROM mytable t
GROUP
BY CASE
WHEN TO_CHAR( t.date_time ,'HH24') < '06' THEN '00:00:00 to 05:59:59'
WHEN TO_CHAR( t.date_time ,'HH24') < '16' THEN '06:00:00 to 15:59:59'
WHEN TO_CHAR( t.date_time ,'HH24') < '24' THEN '16:00:00 to 23:59:59'
END
and add an ORDER BY clause if I want the results returned in a particular order.
If the table contains any NULL values of date_time, the query above will also return a fourth time_range with a NULL value.
Here is how you would get the desired result in three columns (rather than three rows), which makes more sense for most applications. You can change this easily to get the result in rows instead.
Note that if dt is any date in Oracle, dt - trunc(dt) is the number of days (a fraction with value less than 1) since midnight.
select sum(case when dt-trunc(dt) < 6/24 then item else 0 end) as morning,
sum(case when dt-trunc(dt) >= 6/24
and dt-trunc(dt) < 16/24 then item else 0 end) as daytime,
sum(case when dt-trunc(dt) >= 16/24 then item else 0 end) as evening
from your_table
;

If statements in WHERE clause

Is it possible in Oracle to put conditional IF statements in the WHERE clause?
I want to filter all rows with an end date before today. And if the end date is empty, it should not filter on it. I've tried this:
SELECT discount_amount
FROM vw_ph_discount_data
WHERE sysdate > start_date
AND
IF
end_date IS NOT EMPTY
THEN
sysdate < end_date
But I get "invalid relational operator".
You can try:
SELECT discount_amount
FROM vw_ph_discount_data
WHERE sysdate > start_date
AND sysdate < nvl(end_date,sysdate+1)
I don't think that if-else statements can be used in pure Sql code. You need to use stored procedure to achieve your aim. I suppose in your case you can use the code below:
DECLARE
DATE end_date
BEGIN
IF end_date IS NOT NULL THEN
SELECT discount_amount
FROM vw_ph_discount_data
WHERE sysdate > start_date AND sysdate < end_date;
END IF;
END;
Even if it's possible, it's not a good idea. Per-row functions will destroy performance.
In this case, the best way is to probably just union two exclusive queries:
SELECT discount_amount
FROM vw_ph_discount_data
WHERE sysdate > start_date
AND end_date IS NULL
UNION ALL
SELECT discount_amount
FROM vw_ph_discount_data
WHERE sysdate > start_date
AND end_date IS NOT NULL
AND sysdate < end_date
(changed to NULL from EMPTY since that seems to be what you were after).
Assuming end_date is indexed, this should scream along even though it's two queries. Having to do some extra processing on each and every row returned is rarely a good idea.
Whatever methods you choose to investigate, benchmark them with real world data. The prime directive of optimisation is measure, don't guess.
Couldn't you do this:
SELECT discount_amount
FROM vw_ph_discount_data
WHERE sysdate > start_date
AND (end_date IS EMPTY OR sysdate < end_date)
You could use the IF statement to create multiple queries or try WHERE (end_date IS NULL OR end_date > SYSDATE).
Not sure if you should use IS [NOT] EMPTY on "end_date". See IS EMPTY.

Finding a count of rows in an arbitrary date range using Oracle

The question I need to answer is this "What is the maximum number of page requests we have ever received in a 60 minute period?"
I have a table that looks similar to this:
date_page_requested date;
page varchar(80);
I'm looking for the MAX count of rows in any 60 minute timeslice.
I thought analytic functions might get me there but so far I'm drawing a blank.
I would love a pointer in the right direction.
You have some options in the answer that will work, here is one that uses Oracle's "Windowing Functions with Logical Offset" feature instead of joins or correlated subqueries.
First the test table:
Wrote file afiedt.buf
1 create table t pctfree 0 nologging as
2 select date '2011-09-15' + level / (24 * 4) as date_page_requested
3 from dual
4* connect by level <= (24 * 4)
SQL> /
Table created.
SQL> insert into t values (to_date('2011-09-15 11:11:11', 'YYYY-MM-DD HH24:Mi:SS'));
1 row created.
SQL> commit;
Commit complete.
T now contains a row every quarter hour for a day with one additional row at 11:11:11 AM. The query preceeds in three steps. Step 1 is to, for every row, get the number of rows that come within the next hour after the time of the row:
1 with x as (select date_page_requested
2 , count(*) over (order by date_page_requested
3 range between current row
4 and interval '1' hour following) as hour_count
5 from t)
Then assign the ordering by hour_count:
6 , y as (select date_page_requested
7 , hour_count
8 , row_number() over (order by hour_count desc, date_page_requested asc) as rn
9 from x)
And finally select the earliest row that has the greatest number of following rows.
10 select to_char(date_page_requested, 'YYYY-MM-DD HH24:Mi:SS')
11 , hour_count
12 from y
13* where rn = 1
If multiple 60 minute windows tie in hour count, the above will only give you the first window.
This should give you what you need, the first row returned should have
the hour with the highest number of pages.
select number_of_pages
,hour_requested
from (select to_char(date_page_requested,'dd/mm/yyyy hh') hour_requested
,count(*) number_of_pages
from pages
group by to_char(date_page_requested,'dd/mm/yyyy hh')) p
order by number_of_pages
How about something like this?
SELECT TOP 1
ranges.date_start,
COUNT(data.page) AS Tally
FROM (SELECT DISTINCT
date_page_requested AS date_start,
DATEADD(HOUR,1,date_page_requested) AS date_end
FROM #Table) ranges
JOIN #Table data
ON data.date_page_requested >= ranges.date_start
AND data.date_page_requested < ranges.date_end
GROUP BY ranges.date_start
ORDER BY Tally DESC
For PostgreSQL, I'd first probably write something like this for a "window" aligned on the minute. You don't need OLAP windowing functions for this.
select w.ts,
date_trunc('minute', w.ts) as hour_start,
date_trunc('minute', w.ts) + interval '1' hour as hour_end,
(select count(*)
from weblog
where ts between date_trunc('minute', w.ts) and
(date_trunc('minute', w.ts) + interval '1' hour) ) as num_pages
from weblog w
group by ts, hour_start, hour_end
order by num_pages desc
Oracle also has a trunc() function, but I'm not sure of the format. I'll either look it up in a minute, or leave to see a friend's burlesque show.
WITH ranges AS
( SELECT
date_page_requested AS StartDate,
date_page_requested + (1/24) AS EndDate,
ROWNUMBER() OVER(ORDER BY date_page_requested) AS RowNo
FROM
#Table
)
SELECT
a.StartDate AS StartDate,
MAX(b.RowNo) - a.RowNo + 1 AS Tally
FROM
ranges a
JOIN
ranges b
ON a.StartDate <= b.StartDate
AND b.StartDate < a.EndDate
GROUP BY a.StartDate
, a.RowNo
ORDER BY Tally DESC
or:
WITH ranges AS
( SELECT
date_page_requested AS StartDate,
date_page_requested + (1/24) AS EndDate,
ROWNUMBER() OVER(ORDER BY date_page_requested) AS RowNo
FROM
#Table
)
SELECT
a.StartDate AS StartDate,
( SELECT MIN(b.RowNo) - a.RowNo
FROM ranges b
WHERE b.StartDate > a.EndDate
) AS Tally
FROM
ranges a
ORDER BY Tally DESC

Resources