What is a simple way to combine grouped values in one field? - sql

I mean:
Table PHONE_CODES:
ID CODE_NAME PHONE_CODE
1 USA 8101
2 USA 8102
3 PERU 8103
4 PERU_MOB 81031
5 PERU_MOB 81032
And I want via select to get something like this:
CODE_NAME ZONE_CODES
USA 8101; 8102;
PERU 8103
PERU_MOB 81031; 81032;
I could get it via the function below, but perhaps there is a better way:
select distinct(CODE_NAME) as CODE_NAME, get_code_names_by_ZONE(CODE_NAME) as ZONE_CODES from PHONE_CODES;
Function:
create or replace function get_code_names_by_ZONE
(
ZONE_CODE_NAME in varchar2
)
return varchar2
as
codes_list varchar2(4000);
cursor cur_codes_list is
select p.PHONE_CODE
from PHONE_CODES p
where p.CODE_NAME = ZONE_CODE_NAME;
begin
for codes_list_rec in cur_codes_list
LOOP
-- dbms_output.put_line('PHONE_CODE:[' || codes_list_rec.PHONE_CODE || ']');
codes_list := codes_list || codes_list_rec.PHONE_CODE || '; ';
end loop;
return codes_list;
EXCEPTION
when NO_DATA_FOUND then
return 'notfound';
WHEN others then
dbms_output.put_line('Error code:' || SQLCODE || ' msg:' || SQLERRM);
return null;
end get_code_names_by_ZONE;
/

Tim Hall has an excellent discussion on the various string aggregation techniques that are available in Oracle.

A function would be my preferred method of achieving what you want.

If you're on 11g, take a look at the new PIVOT extension to SQL - the best documentation looks to be in the Data Warehousing Guide section. I believe however that the target of the "... for in ..." clause cannot be a subquery and has to be a hard-coded list of values.

Good link Justin. Tim hall is awesome. I followed his advice and here it is:
1 SELECT CODE_NAME,
2 LTRIM(MAX(SYS_CONNECT_BY_PATH(PHONE_CODES,';'))
3 KEEP (DENSE_RANK LAST ORDER BY curr),';') AS PHONE_CODES
4 FROM (SELECT CODE_NAME,
5 PHONE_CODES,
6 ROW_NUMBER() OVER (PARTITION BY CODE_NAME ORDER BY PHONE_CODES) AS curr,
7 ROW_NUMBER() OVER (PARTITION BY CODE_NAME ORDER BY PHONE_CODES) -1 AS prev
8 FROM a)
9 GROUP BY CODE_NAME
10 CONNECT BY prev = PRIOR curr AND CODE_NAME = PRIOR CODE_NAME
11* START WITH curr = 1
SQL> /
CODE_NAME PHONE_CODES
---------- --------------------------------------------------
PERU 8103
PERU_MOB 81031;81032
USA 8101;8102
dbBradley - I don't think the Pivot extension works here. The Pivot extension requires the use of an aggregate (sum, count, ...).

Related

SQL/Power BI How to expand table according dates

I have a table like below, where a new record is created when there is a change in the status of a task.
task
status
last update
A
1
28/04/2022
A
3
01/05/2022
A
5
05/05/2022
B
1
28/04/2022
B
3
03/05/2022
B
4
05/05/2022
The problem is that I need to plot a graph within a time range, where I know the status of each item regardless of the date it was changed/created. With that, I think the easiest is to transform to the table below:
task
status
last update
A
1
28/04/2022
A
1
29/04/2022
A
1
28/04/2022
A
1
29/04/2022
A
1
30/04/2022
A
3
01/05/2022
A
3
02/05/2022
A
3
03/05/2022
A
3
04/05/2022
A
5
05/05/2022
B
1
28/04/2022
B
1
29/04/2022
B
1
30/04/2022
B
1
01/05/2022
B
1
02/05/2022
B
3
03/05/2022
B
3
04/05/2022
B
4
05/05/2022
However, I can't think of a way to do it, either directly in Power BI or even in SQL, since I'm connecting to a redshift database through a sql query.
Could you please help me?
Thanks
You can create the below visual using the standard line chart visualization. In the visualization settings, go to the "Shapes" menu and turn the "Stepped" view on.
While not necessary, it may be best practice to create a date dimension table with daily values spanning from the minimum update date to the maximum update date.
Dates = CALENDAR(MIN(Tasks[last update]),MAX(Tasks[last update]))
You can then create a one to many relationship between Dates and Tasks.
demo
very similar question: How to do forward fill as a PL/PGSQL function
I don't know the actual differences between amazon redshift and postgresql.
The demo is based on postgresql 14. It may not works on redshift.
Basic idea:for every distinct task, get the max, min last_updated date then use the generate_series function to expand the date based on task, task's min & max last_update. key point is first_value(status), because the once you expand the date, then obviously some date the status value is null, then use partition to fill the gap. If you want deep more, you can read manual: https://www.postgresql.org/docs/14/plpgsql.html
CREATE OR REPLACE FUNCTION test_expand ()
RETURNS TABLE (
_date1 date,
_first_ctask text,
_first_cstatus bigint
)
AS $$
DECLARE
distinct_task record;
max_last_update date;
min_last_update date;
_sql text;
BEGIN
FOR distinct_task IN SELECT DISTINCT
task
FROM
test_1
ORDER BY
1 LOOP
min_last_update := (
SELECT
min(last_update)
FROM
test_1
WHERE
task = distinct_task.task
LIMIT 1);
max_last_update := (
SELECT
max(last_update)
FROM
test_1
WHERE
task = distinct_task.task
LIMIT 1);
_sql := format($dml$ WITH cte AS (
SELECT
date1::date, $task$ % s$task $ AS _task, status, count(status) OVER (ORDER BY date1) AS c_s FROM (
SELECT
generate_series($a$ % s$a $::date, $b$ % s$b $::date, interval '1 day')) g (date1)
LEFT JOIN test_1 ON date1 = last_update)
SELECT
date1, _task, first_value(status) OVER (PARTITION BY c_s ORDER BY date1, status)
FROM cte $dml$, distinct_task.task, min_last_update, max_last_update);
RETURN query EXECUTE _sql;
END LOOP;
RETURN;
END;
$$
LANGUAGE plpgsql;

How to add an auto-incremental number at the end of a result?

Is there a way to add a number that auto increments at the end of a select result?
for example, if I have a table called people with a field "name" and I want to select all the names and add a "_x" at the end (x being a number that increases)
name
Richard
John
Peter
Bob
Is there a way to make a
select concat(name, number_that_increments) from peopleand get as result: Richard_1, John_2, Peter_3, Bob_4, etc..?
use this:
select concat(name, '_', row)
from (select row_number() over () as row,* from people) as people_row;
and this is results:
Richard_1
John_2
Peter_3
Bob_4
The function you want is row_number(). In the context of your query:
select name || row_number() over ()
from people
Note:
|| is the standard and Postgres-supported string concatenation operator.
over () provides the enumeration in an arbitrary fashion. If you expect a particular ordering, then use order by <some column or expression>.
The numbers are of different length, so you will get 'X-1', 'X-10', 'X-100', and so on.
If you wanted a separate enumeration (starting at 1) then use partition by name in the windows clause.
Use the function ROW_NUMBER to enumerate your rows, e.g.
CREATE TABLE t (name text);
INSERT INTO t VALUES
('Richard'),('John'),('Peter'),('Bob');
SELECT ROW_NUMBER() OVER (),name FROM t;
row_number | name
------------+---------
1 | Richard
2 | John
3 | Peter
4 | Bob
(4 rows)
Demo: db<>fiddle

Get Distinct Value from an Element in a PL/SQL Associative Array

Say I have the table XX_TABLE_SAMPLE with the records below:
TAB_ID BATCH_NAME EMP_NO EMP_NAME STATUS SALARY CATEGORY
------ ---------- ------ -------- -------- ------- ------------
1 BATCH_A 1 Jared Active 1000 (NULL)
2 BATCH_A 2 Siege Active 3000 (NULL)
3 BATCH_A 45 James Suspended 2000 (NULL)
4 BATCH_B 67 Harry Active 100 (NULL)
5 BATCH_B 99 Pikachu Active 10000 (NULL)
6 BATCH_x 100 Eren Suspended 4000 (NULL)
and i have the PL/SQL block like below (please note the comments):
declare
cursor emp_cur is
select *
from XX_TABLE_SAMPLE
where status = 'Active';
type emp_cur_type is table of XX_TABLE_SAMPLE%rowtype index by pls_integer;
emp_rec emp_cur_type;
begin
open emp_cur;
fetch emp_cur
bulk collect
into emp_rec;
close emp_cur;
/* do some pre-processing here via another stored procedure
but the problem is, it has a parameter of p_batch_name, not a type of associative array
for i in emp_rec.first..emp_rec.last loop
pay_pkg.validate_pay (p_batch_name => emp_rec(i).p_batch_name);
end;
-- the problem with this is that it will loop 4 times (twice each for BATCH_A and BATCH_B)
when it should only get the 2 batch names (BATCH_A and BATCH_B)
*/
-- then check the salary of the emp and update the associative array
for i in emp_rec.first..emp_rec.last loop
if emp_rec(i).salary > 200 and emp_rec(i).salary < 3000 then
emp_rec(i).CATEGORY = 'Manager';
end if;
end loop;
forall i in emp_rec.first..emp_rec.last
update XX_TABLE_SAMPLE
set CATEGORY = emp_rec(i).CATEGORY
where TAB_ID = emp_rec(i).TAB_ID;
end;
With that, I would like to get the distinct values of the Element Batch_Name in an Associative Array
and then pass it to the Stored Procedure pay_pkg.validate_pay.
Any thoughts on how i can achive this without declaring another Explicit Cursor?
To me it seems that you are thinking in unnecessary complex solution. I think your example can be simplified to the following solution that requires zero PL/SQL data structures (r is an implicit record type but the compiler makes it for you!).
Btw, there is no need to be afraid of cursors in PL/SQL.
declare
-- a dummy placeholder for the "tricky" subprogram
procedure validate_pay(p_batch_name in varchar2) is begin null; end;
begin
for r in (select distinct batch_name
from xx_sample_data
where status = 'Active')
loop
validate_pay(p_batch_name => r.batch_name);
end loop;
update xx_sample_data set
category = 'Manager'
where status = 'Active'
and salary between 201 and 2999
;
end;
/
Maybe is something else you aren't saying, but if you need {"BATCH_A", "BATCH_B"}
Why dont just use:
SELECT DISTINCT BATCH_NAME
FROM XX_TABLE_SAMPLE
WHERE status = 'Active'
AND salary > 200
AND salary < 3000
if you are on oracle 12 there is another way.
but it involves selecting from your associative array
see
Select from PLSQL Associative array?

Oracle SQL: How to perform comparison by converting "Varchar" to "Number"

I have only read-only access to Oracle SQL (Can use SELECT command only).
I want to perform the comparison conditions on a Varchar type column by converting it to Number type.
Reference Data:
ID | Price | Currency
-------------------------
548 | 6000 | USD
9784 | 7000 | EUR
254 | 5000 | USD
Query used:
select id, price, currency
from ( select item_id id,
to_number(item_price) price,
item_currency currency
from item
where item_price is not null) A
where A.price <= 6000;
Expected Output:
ID | Price | Currency
-------------------------
548 | 6000 | USD
254 | 5000 | USD
"ORA-01722: invalid number" means what it says: you are attempting to cast a string to a number when the string contains a non-numeric value.
This is the danger of using weakly-typed columns. People always say, "our application will validate the input" . But the one thing you can guarantee is that someone (or something) will stick a non-numeric value into that column.
Okay, so hindsight is a marvellous thing and you probably don't want a lecture from me about data integrity: what, practically, can you do? Basically you need to identify the values which won't cast to numbers and handle them somehow (change the value, filter them from the query, whatever).
There's no Oracle built in to test for numberness but it's easy to write one:
create or replace function is_number (p_str in varchar2)
return varchar2
is
return_value varchar2(5);
n number;
begin
begin
dbms_output.put_line('str='||p_str);
n := to_number(p_str);
return_value := 'TRUE';
exception
when invalid_number or value_error then
dbms_output.put_line('here');
return_value := 'FALSE';
when others then
dbms_output.put_line(sqlerrm);
raise;
end;
return return_value;
end;
/
Here's one way to use it.
with cte as ( select id, price, currency from item
where is_number(price) = 'TRUE')
select id, price, currency
from cte
where to_number(price) <= 6000;
You can use CAST() function.
Example of using it is below
SELECT product_id, CAST(ad_sourcetext AS VARCHAR2(30)) FROM print_media;
For more informations visit THIS.
Hope it helps

How should I proceed after running the collect command?

Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - 64bit Production.
I have a table in the below format.
Number User Value
-------------------
1 A 25
1 B 28
2 C 30
2 D 35
This is what I want:
Number User Value
-------------------
1 A,B 25,28
2 C,D 30,35
I tried using a Listagg but it gives me ORA-01489: result of string concatenation is too long.
This was the listagg command:
SELECT "Number",
LISTAGG ("user", ', ') WITHIN GROUP (ORDER BY "user") "user",
LISTAGG ("value", ', ') WITHIN GROUP (ORDER BY "user") VALUE
FROM table
GROUP BY "Number";
I cant create type as I dont have privileges. In one of the other similar questions, someone suggested using collect. So using this:
SELECT number, CAST (COLLECT (USER) AS SYS.DBMSOUTPUT_LINESARRAY)
FROM emp
GROUP BY number;
I dont know where to go from here. When I run this query, I get this column:
CAST(COLLECT(USER)ASSYS
-----------------------
COLLECTION
COLLECTION
COLLECTION
As you can see my sql knowledge is very minimal. Any help would be much appreciated!
Try using the xmlagg approach:
rtrim(xmlagg(xmlelement(e, name1 || ',')).extract('//text()').getclobval(), ',')
Here is a sqlfiddle demo