removing extra sub-query in Oracle, selecting array of values - sql

I'm SELECTing some aggregate data and grouping on the date and a particular field. I want to display all values in that field and a count for those values even if there was no data matching that field on that day. E.g.
Date MyField Count
2009-09-25 A 2
2009-09-25 B 0
2009-09-24 A 1
2009-09-24 B 1
The Oracle SQL I currently have to do this is akin to the following:
SELECT today,
mytable.myfield,
COUNT(
CASE WHEN fields.myfield = mytable.myfield AND
date >= today AND
date < tomorrow
THEN 1
END
)
FROM (
SELECT TRUNC(SYSDATE) + 1 - LEVEL AS today,
TRUNC(SYSDATE) + 2 - LEVEL AS tomorrow
FROM DUAL
CONNECT BY LEVEL <= 30
),
(
/* This is the part that seems inefficient */
SELECT DISTINCT myfield
FROM mytable
WHERE myfield IN ('A', 'B')
) fields,
mytable
GROUP BY today, mytable.myfield
ORDER BY today DESC, mytable.myfield ASC
My concern is that I know exactly which values I want to display for myfield, and it seems inefficient to have a SELECT query that accesses mytable. I was wondering if there's some way I could do something like this in that sub-query:
SELECT ('A', 'B') AS myfield
FROM DUAL
I'm using an older version of Oracle where WITH clauses do not work.

You would have to get them as different rows, not different columns. So you'll end up with
select 'A' from dual
union
select 'B' from dual
In that case, the query should be equivalent as long as there are rows in mytable with fields 'A' and 'B'. If ever there aren't, then your subquery will return rows that the original subquery would not.

Why don't you upgrade your Oracle Version? The with-clause is added first to Oracle 9.2 (2002). Are you still using Oracle 8?

You don't have a join between the FIELDS sub-query and MYTABLE, so your resultset will contain a row for every value of MYFIELD for the last thirty days.
However, rather than adding that join, why not ditch the sub-query and just filter on MYTABLE.MYFIELD? Also, if you are concerned about performance you should bound the date in a WHERE clause, otherwise you will process every row in MYTABLE.
select today
, myfield
, count ( case when trunc(somedate) = today then 1 end ) as ab_count
from ( select trunc(sysdate) + 1 - level as today
from dual
connect by level <= 30 )
, mytable
where myfield in ('A', 'B')
and somedate >= trunc(sysdate) - 30
group by today, myfield
order by today desc, myfield asc
/
edit
I have run your original query and my revised one against some test data. You will just have to take my word for it that the two resulsets were in fact identical - or try it yourself :)
Your query returns:
TODAY M AB_COUNT
----------- - ----------
26-SEP-2009 A 0
26-SEP-2009 B 0
25-SEP-2009 A 2
25-SEP-2009 B 2
24-SEP-2009 A 2
24-SEP-2009 B 0
...
29-AUG-2009 A 1
29-AUG-2009 B 2
28-AUG-2009 A 1
28-AUG-2009 B 0
60 rows selected.
SQL>
My query returns:
TODAY M AB_COUNT
----------- - ----------
26-SEP-2009 A 0
26-SEP-2009 B 0
25-SEP-2009 A 2
25-SEP-2009 B 2
24-SEP-2009 A 2
24-SEP-2009 B 0
...
29-AUG-2009 A 1
29-AUG-2009 B 2
28-AUG-2009 A 1
28-AUG-2009 B 0
60 rows selected.
SQL>

Related

Oracle Pivot rows to columns pattern matching

I want to rearrange the rows to columns (in tbl2 below) to count the number of occurrences of EXEN for the EXEN col, and any code starting with MPA for the MPACODE column.
SELECT *
FROM (select code from tbl2 where pidm='4062161')
PIVOT (count(*) FOR (code) IN ('EXEN' AS EXEN, 'MPA%' AS MPACODE));
tbl2:
Desired output:
Actual output:
You must perform an intermediate step to transform all MPA%to MPAsee subquery dt2
with dt as (
select 'EXEN' code from dual union all
select 'MPA'||rownum from dual connect by level <= 10),
dt2 as (
select
case when code like 'MPA%' then 'MPA' else code end as code
from dt)
select *
from dt2
pivot (
count(*) for
(code) IN ('EXEN' AS EXEN, 'MPA' AS MPACODE));
EXEN MPACODE
---------- ----------
1 10
PIVOT perform an equal comparison (not LIKE), so this is not valid: 'MPA%' AS MPACODE and the reason why the query fails.
for example:
select
count(case when code='EXEN' then 1 end) exen,
count(case when code like 'MPA%' then 1 end) mpacode
from tbl2 where pidm='4062161';

How to union a hardcoded row after each grouped result

After every group / row i want to insert a hardcoded dummy row with a bunch of 'xxxx' to act a separator.
I would like to use oracle sql to do this query. i can execute it using a loop but i don't want to use plsql.
As the others suggest, it is best to do it on the front end.
However, if you have a burning need to be done as a query, here is how.
Here I did not use the rownum function as you have already done. I assume, your data is returned by a query, and you can replace my table with your query.
I made few more assumptions, as you have data with row numbers in it.
[I am not sure what do you mean by not PL/SQL]
Select Case When MOD(rownm, 2) = 0 then ' '
Else to_char((rownm + 1) / 2) End as rownm,
name, total, column1
From
(
select (rownm * 2 - 1) rownm,name, to_char(total) total ,column1 from t
union
SELECT (rownm * 2) rownm,'XXX' name, 'XXX' total, 'The row act .... ' column1 FROM t
) Q
Order by Q.rownm;
and here is the fiddle
Since you're already grouping the data, it might be easier to use GROUPING SETS instead of a UNION.
Grouping sets let you group by multiple sets of columns, including the same set twice to duplicate rows. Then the GROUP_ID function can be used to determine when the fake values should be used. This code will be a bit smaller than a UNION approach, and should be faster since it doesn't need to reference the table multiple times.
select
case when group_id() = 0 then name else '' end name,
case when group_id() = 0 then sum(some_value) else null end total,
case when group_id() = 1 then 'this rows...' else '' end column1
from
(
select 'jack' name, 22 some_value from dual union all
select 'jack' name, 1 some_value from dual union all
select 'john' name, 44 some_value from dual union all
select 'john' name, 1 some_value from dual union all
select 'harry' name, 1 some_value from dual union all
select 'harry' name, 1 some_value from dual
) raw_data
group by grouping sets (name, name)
order by raw_data.name, group_id();
You can use row generator technique (using CONNECT BY) and then use CASE..WHEN as follows:
SQL> SELECT CASE WHEN L.LVL = 1 THEN T.ROWNM END AS ROWNM,
2 CASE WHEN L.LVL = 1 THEN T.NAME
3 ELSE 'XXX' END AS NAME,
4 CASE WHEN L.LVL = 1 THEN TO_CHAR(T.TOTAL)
5 ELSE 'XXX' END AS TOTAL,
6 CASE WHEN L.LVL = 1 THEN T.COLUMN1
7 ELSE 'This row act as separator..' END AS COLUMN1
8 FROM T CROSS JOIN (
9 SELECT LEVEL AS LVL FROM DUAL CONNECT BY LEVEL <= 2
10 ) L ORDER BY T.ROWNM, L.LVL;
ROWNM NAME TOTAL COLUMN1
---------- ---------- ----- ---------------------------
1 Jack 23
XXX XXX This row act as separator..
2 John 45
XXX XXX This row act as separator..
3 harry 2
XXX XXX This row act as separator..
4 roy 45
XXX XXX This row act as separator..
5 Jacob 26
XXX XXX This row act as separator..
10 rows selected.
SQL>

SQL : how to find gaps in a specific range of numbers?

I want to find gaps in a numeric column (not the entire column), but in a specific range.
For example :
My column :
1
2
5
6
8
10
18
19
20
I want to specify a specific range in which my SQL query would look for gaps. For example, I want gaps in the range [15,20]. In this example, the gaps are : 15,16,17.
I built a query that retrieves gaps but not in case the gaps are at the beginning of my range
SELECT cur_value + 1 AS start_gap, next_value - 1 AS end_gap
FROM (
SELECT col AS cur_value, LEAD (col) OVER (ORDER BY col) AS next_value
FROM table
--WHERE col BETWEEN 200 AND 300
)
WHERE next_value - cur_value > 1
ORDER BY start_gap;
How can I do that ?
N.B : Performance is very important in my case. I deal with tons of rows.
I think the simplest method might be:
SELECT cur_value + 1 AS start_gap, next_value - 1 AS end_gap
FROM (SELECT col AS cur_value, LEAD (col) OVER (ORDER BY col) AS next_value
FROM (SELECT col
FROM table
WHERE col BETWEEN 200 AND 300
UNION ALL
SELECT 200-1 FROM DUAL
UNION ALL
SELECT 300+1 FROM DUAL
) t
) t
WHERE next_value - cur_value > 1
ORDER BY start_gap;
Note: This will work on arbitrarily long ranges.
You can simply use hierarchical connect by to produce the list of numbers in the given range and then check to see if the value doesn't exist in the table.
Assuming your range is 15 to 20, use NOT IN:
select * from (
select level - 1 + 15 col
from dual
connect by level <= 20 - 15
) where col not in (select col from your_table);
Demo
Similarly, NOT EXISTS:
select * from (
select level - 1 + 15 col
from dual
connect by level <= 20 - 15
) t where not exists (select 1 from your_table where col = t.col);
Demo 2

SQL query : how to check existence of multiple rows with one query

I have this table MyTable:
PROG VALUE
-------------
1 aaaaa
1 bbbbb
2 ccccc
4 ddddd
4 eeeee
now I'm checking the existence of a tuple with a certain id with a query like
SELECT COUNT(1) AS IT_EXISTS
FROM MyTable
WHERE ROWNUM = 1 AND PROG = {aProg}
For example I obtain with aProg = 1 :
IT_EXISTS
---------
1
I get with aProg = 3 :
IT_EXISTS
---------
0
The problem is that I must do multiple queries, one for every value of PROG to check.
What I want is something that with a query like
SELECT PROG, ??? AS IT_EXISTS
FROM MyTable
WHERE PROG IN {1, 2,3, 4, 5} AND {some other condition}
I can get something like
PROG IT_EXISTS
------------------
1 1
2 1
3 0
4 1
5 0
The database is Oracle...
Hope I'm clear
regards
Paolo
Take a step back and ask yourself this: Do you really need to return the rows that don't exist to solve your problem? I suspect the answer is no. Your application logic can determine that records were not returned which will allow you to simplify your query.
SELECT PROG
FROM MyTable
WHERE PROG IN (1, 2, 3, 4, 5)
If you get a row back for a given PROG value, it exists. If not, it doesn't exist.
Update:
In your comment in the question above, you stated:
the prog values are from others tables. The table of the question has only a subset of the all prog values
This suggests to me that a simple left outer join could do the trick. Assuming your other table with the PROG values you're interested in is called MyOtherTable, something like this should work:
SELECT a.PROG,
CASE WHEN b.PROG IS NOT NULL THEN 1 ELSE 0 END AS IT_EXISTS
FROM MyOtherTable AS a
LEFT OUTER JOIN MyTable AS b ON b.PROG = a.PROG
A WHERE clause could be tacked on to the end if you need to do some further filtering.
I would recommend something like this. If at most one row can match a prog in your table:
select p.prog,
(case when t.prog is null then 0 else 1 end) as it_exists
from (select 1 as prog from dual union all
select 2 as prog from dual union all
select 3 as prog from dual union all
select 4 as prog from dual union all
select 5 as prog from dual
) p left join
mytable t
on p.prog = t.prog and <some conditions>;
If more than one row could match, you'll want to use aggregation to avoid duplicates:
select p.prog,
max(case when t.prog is null then 0 else 1 end) as it_exists
from (select 1 as prog from dual union all
select 2 as prog from dual union all
select 3 as prog from dual union all
select 4 as prog from dual union all
select 5 as prog from dual
) p left join
mytable t
on p.prog = t.prog and <some conditions>
group by p.prog
order by p.prog;
One solution is to use (arguably abuse) a hierarchical query to create an arbitrarily long list of numbers (in my example, I've set the largest number to max(PROG), but you could hardcode this if you knew the top range you were looking for). Then select from that list and use EXISTS to check if it exists in MYTABLE.
select
PROG
, case when exists (select 1 from MYTABLE where PROG = A.PROG) then 1 else 0 end IT_EXISTS
from (
select level PROG
from dual
connect by level <= (select max(PROG) from MYTABLE) --Or hardcode, if you have a max range in mind
) A
;
It's still not very clear where you get the prog values to check. But if you can read them from a table, and assuming that the table doesn't contain duplicate prog values, this is the query I would use:
select a.prog, case when b.prog is null then 0 else 1 end as it_exists
from prog_values_to_check a
left join prog_values_to_check b
on a.prog = b.prog
and exists (select null
from MyTable t
where t.prog = b.prog)
If you do need to hard code the values, you can do it rather simply by taking advantage of the SYS.DBMS_DEBUG_VC2COLL function, which allows you to convert a comma-delimited list of values into rows.
with prog_values_to_check(prog) as (
select to_number(column_value) as prog
from table(SYS.DBMS_DEBUG_VC2COLL(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) -- type your values here
)
select a.prog, case when b.prog is null then 0 else 1 end as it_exists
from prog_values_to_check a
left join prog_values_to_check b
on a.prog = b.prog
and exists (select null
from MyTable t
where t.prog = b.prog)
Note: The above queries take into account that the MyTable table may have multiple rows with the same prog value, but that you only want one row in the result. I make this assumption based the WHERE ROWNUM = 1 condition in your question.

T-SQL "Dynamic" Join

Given the following SQL Server table with a single char(1) column:
Value
------
'1'
'2'
'3'
How do I obtain the following results in T-SQL?
Result
------
'1+2+3'
'1+3+2'
'2+1+3'
'2+3+1'
'3+2+1'
'3+1+2'
This needs to be dynamic too, so if my table only holds rows '1' and '2' I'd expect:
Result
------
'1+2'
'2+1'
It seems like I should be able to use CROSS JOIN to do this, but since I don't know how many rows there will be ahead of time, I'm not sure how many times to CROSS JOIN back on myself..?
SELECT a.Value + '+' + b.Value
FROM MyTable a
CROSS JOIN MyTable b
WHERE a.Value <> b.Value
There will always be less than 10 (and really more like 1-3) rows at any given time. Can I do this on-the-fly in SQL Server?
Edit: ideally, I'd like this to happen in a single stored proc, but if I have to use another proc or some user defined functions to pull this off I'm fine with that.
This SQL will compute the permutations without repetitions:
WITH recurse(Result, Depth) AS
(
SELECT CAST(Value AS VarChar(100)), 1
FROM MyTable
UNION ALL
SELECT CAST(r.Result + '+' + a.Value AS VarChar(100)), r.Depth + 1
FROM MyTable a
INNER JOIN recurse r
ON CHARINDEX(a.Value, r.Result) = 0
)
SELECT Result
FROM recurse
WHERE Depth = (SELECT COUNT(*) FROM MyTable)
ORDER BY Result
If MyTable contains 9 rows, it will take some time to compute, but it will return 362,880 rows.
Update with explanation:
The WITH statement is used to define a recursive common table expression. In effect, the WITH statement is looping multiple times performing a UNION until the recursion is finished.
The first part of SQL sets the starting records. Assuming 3 rows named 'A', 'B', and 'C' in MyTable, this will generate these rows:
Result Depth
------ -----
A 1
B 1
C 1
Then the next block of SQL performs the first level of recursion:
SELECT CAST(r.Result + '+' + a.Value AS VarChar(100)), r.Depth + 1
FROM MyTable a
INNER JOIN recurse r
ON CHARINDEX(a.Value, r.Result) = 0
This takes all of the records generated so far (which will be in the recurse table) and joins them to all of the records in MyTable again. The ON clause filters the list of records in MyTable to only return the ones that do not exist already in this row's permutation. This would result in these rows:
Result Depth
------ -----
A 1
B 1
C 1
A+B 2
A+C 2
B+A 2
B+C 2
C+A 2
C+B 2
Then the recursion loops again giving these rows:
Result Depth
------ -----
A 1
B 1
C 1
A+B 2
A+C 2
B+A 2
B+C 2
C+A 2
C+B 2
A+B+C 3
A+C+B 3
B+A+C 3
B+C+A 3
C+A+B 3
C+B+A 3
At this point, the recursion stops because the UNION does not create any more rows because the CHARINDEX will always be 0.
The last SQL filters all of the resulting rows where the computed Depth column matches the # of records in MyTable. This throws out all of the rows except for the ones generated by the last depth of recursion. So the final result will be these rows:
Result
------
A+B+C
A+C+B
B+A+C
B+C+A
C+A+B
C+B+A
You can do this with a recursive CTE:
with t as (
select 'a' as value union all
select 'b' union all
select 'c'
),
const as (select count(*) as cnt from t),
cte as (
select cast(value as varchar(max)) as value, 1 as level
from t
union all
select cte.value + '+' + t.value, 1 + level
from cte join
t
on '+'+cte.value+'+' not like '%+'+t.value+'+%' cross join
const
where level <= const.cnt
)
select cte.value
from cte cross join
const
where level = const.cnt;