Oracle Pivot rows to columns pattern matching - sql

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';

Related

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>

Non duplicate records with max date query on oracle

Hello i have a problem with a simple query. I need to see the max date of some articles in two direfent sites.
This is my actual query:
SELECT a.aa_codart, MAX(t.tr_fechafac), t.tr_tipo
FROM ARTALM a, traspaso t
WHERE t.tr_codart = a.aa_codart
and t.tr_tipomov > 1
and a.aa_codalm = '1'
and (t.tr_tipo >= 1 and t.tr_tipo <=2)
group by a.aa_codart, t.tr_tipo;
And the result:
01..FRB10X80 30/11/07 2
01..FRB10X80 08/03/01 1
01.32122RS 05/02/16 1
01.32122RS 02/07/10 2
01.33052Z 21/09/15 1
01.60042Z 24/02/16 2
I want, for example in the two first rows, see only one row, like this:
01..FRB10X80 30/11/07 2
01.32122RS 05/02/16 1
01.33052Z 21/09/15 1
01.60042Z 24/02/16 2
Taking the max date
Thanks
This calls for an analytical query. This query shows how the ROW_NUMBER() function will assign the value 1 to the row with the article's most recent date. Give it a try first to help understand the final query, coming up next:
SELECT
a.aa_codart,
t.tr_fechafac,
t.tr_tipo,
ROW_NUMBER() OVER (PARTITION BY a.aa_codart ORDER BY t.tr_fechafac DESC) as rnk
FROM artalm a
INNER JOIN trapaso t ON a.aa_codart = t.tr_codart
WHERE t.tr_tipomov > 1
AND a.aa_codalm = '1'
AND t.tr_tipo BETWEEN 1 AND 2
You can't apply the WHERE clause to the rnk column because the column is calculated after the WHERE clause. You can get around this using a nested query:
SELECT * FROM (
SELECT
a.aa_codart,
t.tr_fechafac,
t.tr_tipo,
ROW_NUMBER() OVER (PARTITION BY a.aa_codart ORDER BY t.tr_fechafac DESC) as rnk
FROM artalm a
INNER JOIN trapaso t ON a.aa_codart = t.tr_codart
WHERE t.tr_tipomov > 1
AND a.aa_codalm = '1'
AND t.tr_tipo BETWEEN 1 AND 2
) WHERE rnk = 1;
I apologize in advance for any column names I may have retyped badly. The Oracle syntax should be fine; the column names maybe not so much :)
I think you may want to look at row_number() (then just pick the ones where it is one) something like this.
WITH t
AS (SELECT 'A' aa_codart,
TO_DATE ('17/05/00', 'dd/mm/yy') mydt,
1 tr_tipo
FROM DUAL
UNION ALL
SELECT 'A', TO_DATE ('12/04/00', 'dd/mm/yy'), 2 FROM DUAL
UNION ALL
SELECT 'B', TO_DATE ('30/06/98', 'dd/mm/yy'), 2 FROM DUAL
UNION ALL
SELECT 'C', TO_DATE ('30/06/98 ', 'dd/mm/yy'), 2 FROM DUAL),
t2
AS (SELECT aa_codart,
mydt,
tr_tipo,
ROW_NUMBER ()
OVER (PARTITION BY aa_codart ORDER BY mydt DESC)
rn
FROM t)
SELECT *
FROM t2
WHERE rn = 1

Case statement not supporting horizontal search with column name in query

I am new to ORACLE SQL and I am trying to learn it quickly.
I have following table definition:
Create table Sales_Biodata
(
Saler_Id INTEGER NOT NULL UNIQUE,
Jan_Sales INTEGER NOT NULL,
Feb_Sales INTEGER NOT NULL,
March_Sales INTEGER NOT NULL
);
Insert into Sales_Biodata (SALER_ID,JAN_SALES,Feb_Sales,March_Sales)
values ('101',22,525,255);
Insert into Sales_Biodata (SALER_ID,JAN_SALES,Feb_Sales,March_Sales)
values ('102',22,55,25);
Insert into Sales_Biodata (SALER_ID,JAN_SALES,Feb_Sales,March_Sales)
values ('103',45545,5125,2865);
My objective is the following:
1- Searching the highest sales and second highest sales against each saler_id.
For example in our above case:
For saler_id =101 highest sales is 525 and second highest sales is 255
similary for saler_id=102 highest sales is 55 and second highest sales is 25
For my above approach I am using the following query:
Select Saler_Id,
(
CASE
WHEN JAN_SALES>FEB_SALES AND JAN_SALES>MARCH_SALES THEN JAN_SALES
WHEN FEB_SALES>JAN_SALES AND FEB_SALES>MARCH_SALES THEN FEB_SALES
WHEN MARCH_SALES>JAN_SALES AND MARCH_SALES>FEB_SALES THEN MARCH_SALES
WHEN JAN_SALES=FEB_SALES AND JAN_SALES=MARCH_SALES THEN JAN_SALES
WHEN JAN_SALES=FEB_SALES AND JAN_SALES>MARCH_SALES THEN JAN_SALES
WHEN JAN_SALES=MARCH_SALES AND JAN_SALES>FEB_SALES THEN JAN_SALES
WHEN FEB_SALES=JAN_SALES AND FEB_SALES>MARCH_SALES THEN FEB_SALES
WHEN FEB_SALES=MARCH_SALES AND FEB_SALES>JAN_SALES THEN FEB_SALES
WHEN MARCH_SALES=JAN_SALES AND MARCH_SALES>FEB_SALES THEN MARCH_SALES
WHEN MARCH_SALES=FEB_SALES AND MARCH_SALES>JAN_SALES THEN MARCH_SALES
ELSE 'NEW_CASE_FOUND'
END
) FIRST_HIGHEST,
(
CASE
WHEN JAN_SALES>FEB_SALES AND FEB_SALES>MARCH_SALES THEN FEB_SALES
WHEN FEB_SALES>JAN_SALES AND JAN_SALES>MARCH_SALES THEN JAN_SALES
WHEN JAN_SALES>MARCH_SALES AND MARCH_SALES>FEB_SALES THEN MARCH_SALES
ELSE 'NEW_CASE_FOUND'
END
) SECOND_HIGHEST
from
Sales_Biodata;
but I am getting the following errors:
ORA-00932: inconsistent datatypes: expected NUMBER got CHAR
00932. 00000 - "inconsistent datatypes: expected %s got %s"
*Cause:
*Action:
Error at Line: 60 Column: 6
Please guide me on the following:
1- How to search the data horizontally for maximum and second maximum.
2- Please guide me on alternate approaches for searching data for a row horizontally.
Getting the maximum value is simply:
select greatest(jan_sales, feb_sales, mar_sales)
If you want the second value:
select (case when jan_sales = greatest(jan_sales, feb_sales, mar_sales)
then greatest(feb_sales, mar_sales)
when feb_sales = greatest(jan_sales, feb_sales, mar_sales)
then greatest(jan_sales, mar_sales)
else greatest(jan_sales, feb_sales)
end)
However, this is the wrong approach to the whole problem. The main issues is that you have the wrong data structure. Store values in rows not columns. So, you need to unpivot your data and re-aggregation, such as:
select saler_id,
max(case when seqnum = 1 then sales end) as sales_1,
max(case when seqnum = 2 then sales end) as sales_2,
max(case when seqnum = 3 then sales end) as sales_3
from (select s.*, dense_rank() over (partition by saler_id order by sales desc) as seqnum
from (select saler_id, jan_sales as sales Sales_Biodata union all
select saler_id, feb_sales Sales_Biodata union all
select saler_id, mar_sales Sales_Biodata
) s
) s
group by saler_id;
Your data model is wrong.
The first thing I would do is to unpivot data using this query:
select * from sales_biodata
unpivot (
val for mon in ( JAN_SALES,FEB_SALES,MARCH_SALES )
)
;
and after this, getting two top values is relatively easy:
SELECT *
FROM (
SELECT t.*,
dense_rank() over (partition by saler_id order by val desc ) x
FROM (
select * from sales_biodata
unpivot (
val for mon in ( JAN_SALES,FEB_SALES,MARCH_SALES )
)
) t
)
WHERE x <= 2
the above query will give a result in this format:
SALER_ID MON VAL X
---------- ----------- ---------- ----------
101 FEB_SALES 525 1
101 MARCH_SALES 255 2
102 FEB_SALES 55 1
102 MARCH_SALES 25 2
103 JAN_SALES 45545 1
103 FEB_SALES 5125 2
If you have more month than 3 months, you can easily extend this query changing this part:
val for mon in ( JAN_SALES,FEB_SALES,MARCH_SALES, April_sales, MAY_SALES, JUNE_SALES, JULY_SALES, ...... NOVEMBER_SALES, DECEMBER_SALES )
If you want both two values in one row, you need to pivot data back:
WITH src_data AS(
SELECT saler_id, val, x
FROM (
SELECT t.*,
dense_rank() over (partition by saler_id order by val desc ) x
FROM (
select * from sales_biodata
unpivot (
val for mon in ( JAN_SALES,FEB_SALES,MARCH_SALES )
)
) t
)
WHERE x <= 2
)
SELECT *
FROM src_data
PIVOT(
max(val) FOR x IN ( 1 As "First value", 2 As "Second value" )
);
This gives a result in this form:
SALER_ID First value Second value
---------- ----------- ------------
101 525 255
102 55 25
103 45545 5125
EDIT - why MAX is used in the PIVOT query
The short answer is: because the syntax reuires an aggregate function here.
See this link for the syntax: http://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_10002.htm#CHDCEJJE
A broader answer:
The PIVOT clause is only a syntactic sugar that simplifies a general "classic" pivot query which is using aggregate function and GROUP BY clause, like this:
SELECT id,
max( CASE WHEN some_column = 'X' THEN value END ) As x,
max( CASE WHEN some_column = 'Y' THEN value END ) As y,
max( CASE WHEN some_column = 'Z' THEN value END ) As z
FROM table11
GROUP BY id
More on PIVOT queries you can find on the net, there is a lot of excelent explanations how the pivot query works.
The above pivot query, written in "standard" SQL, is equivalent to this Oracle's query:
SELECT *
FROM table11
PIVOT (
max(value) FOR some_column IN ( 'X', 'Y', 'Z' )
)
These PIVOT queries transform records like this:
ID SOME_COLUMN VALUE
---------- ----------- ----------
1 X 10
1 X 15
1 Y 20
1 Z 30
into one record (for each id) like this:
ID 'X' 'Y' 'Z'
---------- ---------- ---------- ----------
1 15 20 30
Please note, that the source table contains two values for id=1 and some_column='X' -> 10 and 15. PIVOT queries uses aggregate function to support that "general" case, where there could be many source records for one record in the output. In this example 'MAX' function is used to pick greater value 15.
However PIVOT queries supports also your specific case where there is only one source record for each value in the result.
You are coming across the error as the string 'new case found' is added in the else part and the rest of the case statement deals with number . data type in the when and else clause should match.
Coming to alternate approaches you may use unpivot and get the months sales data into a single row and use analytical functions to get the 1st highest or second highest.
As others have said, the problem is that the WHEN clauses in your CASE statement are returning INTEGER values, but the ELSE is returning a character string. I completely agree with the comments regarding normalization but if you really just want to make this query work you'll need to convert the results of each WHEN clause to character, as in:
Select Saler_Id,
(
CASE
WHEN JAN_SALES>FEB_SALES AND JAN_SALES>MARCH_SALES THEN TO_CHAR(JAN_SALES)
WHEN FEB_SALES>JAN_SALES AND FEB_SALES>MARCH_SALES THEN TO_CHAR(FEB_SALES)
WHEN MARCH_SALES>JAN_SALES AND MARCH_SALES>FEB_SALES THEN TO_CHAR(MARCH_SALES)
WHEN JAN_SALES=FEB_SALES AND JAN_SALES=MARCH_SALES THEN TO_CHAR(JAN_SALES)
WHEN JAN_SALES=FEB_SALES AND JAN_SALES>MARCH_SALES THEN TO_CHAR(JAN_SALES)
WHEN JAN_SALES=MARCH_SALES AND JAN_SALES>FEB_SALES THEN TO_CHAR(JAN_SALES)
WHEN FEB_SALES=JAN_SALES AND FEB_SALES>MARCH_SALES THEN TO_CHAR(FEB_SALES)
WHEN FEB_SALES=MARCH_SALES AND FEB_SALES>JAN_SALES THEN TO_CHAR(FEB_SALES)
WHEN MARCH_SALES=JAN_SALES AND MARCH_SALES>FEB_SALES THEN TO_CHAR(MARCH_SALES)
WHEN MARCH_SALES=FEB_SALES AND MARCH_SALES>JAN_SALES THEN TO_CHAR(MARCH_SALES)
ELSE 'NEW_CASE_FOUND'
END
) FIRST_HIGHEST,
(
CASE
WHEN JAN_SALES>FEB_SALES AND FEB_SALES>MARCH_SALES THEN TO_CHAR(FEB_SALES)
WHEN FEB_SALES>JAN_SALES AND JAN_SALES>MARCH_SALES THEN TO_CHAR(JAN_SALES)
WHEN JAN_SALES>MARCH_SALES AND MARCH_SALES>FEB_SALES THEN TO_CHAR(MARCH_SALES)
ELSE 'NEW_CASE_FOUND'
END
) SECOND_HIGHEST
from
Sales_Biodata;
Best of luck.

Get every combination of sort order and value of a csv

If I have a string with numbers separated by commas, like this:
Declare #string varchar(20) = '123,456,789'
And would like to return every possible combination + sort order of the values by doing this:
Select Combination FROM dbo.GetAllCombinations(#string)
Which would in result return this:
123
456
789
123,456
456,123
123,789
789,123
456,789
789,456
123,456,789
123,789,456
456,789,123
456,123,789
789,456,123
789,123,456
As you can see not only is every combination returned, but also each combination+sort order as well. The example shows only 3 values separated by commas, but should parse any amount--Recursive.
The logic needed would be somewhere in the realm of using a WITH CUBE statement, but the problem with using WITH CUBE (in a table structure instead of CSV of course), is that it won't shuffle the order of the values 123,456 456,123 etc., and will only provide each combination, which is only half of the battle.
Currently I have no idea what to try. If someone can provide some assistance it would be appreciated.
I use a User Defined Table-valued Function called split_delimiter that takes 2 values: the #delimited_string and the #delimiter_type.
CREATE FUNCTION [dbo].[split_delimiter](#delimited_string VARCHAR(8000), #delimiter_type CHAR(1))
RETURNS TABLE AS
RETURN
WITH cte10(num) AS
(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
,cte100(num) AS
(
SELECT 1
FROM cte10 t1, cte10 t2
)
,cte10000(num) AS
(
SELECT 1
FROM cte100 t1, cte100 t2
)
,cte1(num) AS
(
SELECT TOP (ISNULL(DATALENGTH(#delimited_string),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM cte10000
)
,cte2(num) AS
(
SELECT 1
UNION ALL
SELECT t.num+1
FROM cte1 t
WHERE SUBSTRING(#delimited_string,t.num,1) = #delimiter_type
)
,cte3(num,[len]) AS
(
SELECT t.num
,ISNULL(NULLIF(CHARINDEX(#delimiter_type,#delimited_string,t.num),0)-t.num,8000)
FROM cte2 t
)
SELECT delimited_item_num = ROW_NUMBER() OVER(ORDER BY t.num)
,delimited_value = SUBSTRING(#delimited_string, t.num, t.[len])
FROM cte3 t;
Using that I was able to parse the CSV to a table and join it back to itself multiple times and use WITH ROLLUP to get the permutations you are looking for.
WITH Numbers as
(
SELECT delimited_value
FROM dbo.split_delimiter('123,456,789',',')
)
SELECT CAST(Nums1.delimited_value AS VARCHAR)
,ISNULL(CAST(Nums2.delimited_value AS VARCHAR),'')
,ISNULL(CAST(Nums3.delimited_value AS VARCHAR),'')
,CAST(Nums4.delimited_value AS VARCHAR)
FROM Numbers as Nums1
LEFT JOIN Numbers as Nums2
ON Nums2.delimited_value not in (Nums1.delimited_value)
LEFT JOIN Numbers as Nums3
ON Nums3.delimited_value not in (Nums1.delimited_value, Nums2.delimited_value)
LEFT JOIN Numbers as Nums4
ON Nums4.delimited_value not in (Nums1.delimited_value, Nums2.delimited_value, Nums3.delimited_value)
GROUP BY CAST(Nums1.delimited_value AS VARCHAR)
,ISNULL(CAST(Nums2.delimited_value AS VARCHAR),'')
,ISNULL(CAST(Nums3.delimited_value AS VARCHAR),'')
,CAST(Nums4.delimited_value AS VARCHAR) WITH ROLLUP
If you will potentially have more than 3 or 4, you'll want to expand your code accordingly.

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

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>