Dynamic Pivotting in PostgreSQL - sql

I'd like get a count of every possible value from one table associated with each possible value from another table. So if my (combined) table basically look like:
Order ID Employee Product Category
-------------------------------------------
1 Alan Automobile
2 Barry Beauty
3 Charlie Clothing
4 Alan Beauty
I would like to be able to query and get a result of:
Employee Count Auto Count Beauty Count Clothing
------------------------------------------------------------
Alan 1 1 0
Barry 0 1 0
Charlie 0 0 1
I could manually query for each count, but then if I later add new product categories, it will no longer work. What I'm doing now is basically just:
SELECT employee, category, COUNT(*) FROM sales GROUP BY employee, category;
Which returns:
Employee Category Count
-------------------------------
Alan Automobile 1
Alan Beauty 1
Alan Clothing 0
etc. But with a large number of categories this can get a bit redundant. Is there any way to have it returned as a single row for each employee with a column for every category?

You can use JSON approach
SELECT employee,
json_object_agg(ProductCategory,total ORDER BY ProductCategory)
FROM (
SELECT employee, ProductCategory, count(*) AS total
FROM tbl
GROUP BY employee,ProductCategory
) s
GROUP BY employee
ORDER BY employee;
or with two step approach
CREATE FUNCTION dynamic_pivot(central_query text, headers_query text)
RETURNS refcursor AS
$$
DECLARE
left_column text;
header_column text;
value_column text;
h_value text;
headers_clause text;
query text;
j json;
r record;
curs refcursor;
i int:=1;
BEGIN
-- find the column names of the source query
EXECUTE 'select row_to_json(_r.*) from (' || central_query || ') AS _r' into j;
FOR r in SELECT * FROM json_each_text(j)
LOOP
IF (i=1) THEN left_column := r.key;
ELSEIF (i=2) THEN header_column := r.key;
ELSEIF (i=3) THEN value_column := r.key;
END IF;
i := i+1;
END LOOP;
-- build the dynamic transposition query (based on the canonical model)
FOR h_value in EXECUTE headers_query
LOOP
headers_clause := concat(headers_clause,
format(chr(10)||',min(case when %I=%L then %I::text end) as %I',
header_column,
h_value,
value_column,
h_value ));
END LOOP;
query := format('SELECT %I %s FROM (select *,row_number() over() as rn from (%s) AS _c) as _d GROUP BY %I order by min(rn)',
left_column,
headers_clause,
central_query,
left_column);
-- open the cursor so the caller can FETCH right away
OPEN curs FOR execute query;
RETURN curs;
END
$$ LANGUAGE plpgsql;
then
=> BEGIN;
-- step 1: get the cursor (we let Postgres generate the cursor's name)
=> SELECT dynamic_pivot(
'SELECT employee,ProductCategory,count(*)
FROM tbl GROUP BY employee,ProductCategory
ORDER BY 1',
'SELECT DISTINCT productCategory FROM tbl ORDER BY 1'
) AS cur
\gset
-- step 2: read the results through the cursor
=> FETCH ALL FROM :"cur";
Reference

Related

SQL QUERY: How to assign random different value of column in a table, to differents row in another table?

I'm developing a desktop application that works like Risiko!'s game, based on some database procedures.
I need to assign to each player a tot of rows which are not equal to each other assigned to other players.
I've tried for something like this, but doesn't work:
`create or replace PROCEDURE "giveterritory"
(idpartita NUMBER,nr_partecipanti number )
is
begin
DECLARE
CURSOR cur_file IS
SELECT *
FROM Player
WHERE idgame = Player.idgame ;
var_cur cur_file%ROWTYPE;
cont number ;
nr_territories_gioc_prec number ;
G1_Terr int ;
G2_Terr int ;
G3_Terr int ;
G1_ID number;
G2_ID number;
G3_ID number;
begin
cont:=0 ;
for var_cur in cur_file
loop
cont := cont + 1 ;
update territory
set Player_owner = var_cur.id_Player
where ROWNUM = MOD( MOD ( number_RANDOM(),42 ) ,ROWNUM );
G1_Terr := var_cur.number_territories_tot ; -- mi salvo i territories del primo Player
G1_ID := var_cur.id_Player ;
end if ;
if (cont=2 ) then
update territory
set Player_owner = var_cur.id_Player
where ROWNUM = MOD( MOD ( number_RANDOM(),42 -G1_Terr ) , ROWNUM ) and id_territory NOT IN ( select Player_owner from territory where ;
G2_Terr := var_cur.number_territories_tot ;
end if ;
if (cont=3 ) then
update territory
set Player_owner = var_cur.id_Player
where ROWNUM <= 42 - G1_Terr - G2_Terr;
--where ROWNUM <= 42 - var_cur.number_territories_tot * 2 ;
G3_Terr := var_cur.number_territories_tot ;
end if ;
end loop;
end;
end;`
number_random code:
create or replace FUNCTION "NUMBER_RANDOM"
return NUMBER
is
numbrndm int;
begin
select dbms_random.value(1,99999999) into numbrndm
from dual;
return numbrndm;
end;
If I understand correctly, you're trying to assign each player a random set of territories?
If so, sort the territories in random order and assign them to players round robin. Here's a MERGE statement that should do that. I apologize for any minor syntax errors: I am not in front of an Oracle database right now.
MERGE INTO territory t
USING (
WITH players AS ( SELECT rownum player#, id_player
FROM player
WHERE idgame=:idgame ),
players_count AS ( SELECT count(*) players_count FROM players )
-- Get the id_player for each assignment
SELECT assignedt.id_territory, p.id_player
FROM
(
-- Assign the randomly-ordered territories to players in round-robin format
SELECT randt.id_territory,
mod(rownum, pc.players_count)+1 player#
FROM (
-- Query the territories in a random order
SELECT t.id_territory
FROM territory t
ORDER BY dbms_random.random ) randt, players_count pc
-- Optional:
-- ... add a where clause to limit rownum <= some number to make
-- ... sure each player receives an even number of territories.
-- ... You'd need the count of territories to do that.
) assignedt inner join players p on p.player# = assignedt.player#
) u
ON ( u.id_territory = t.id_territory )
WHEN MATCHED THEN UPDATE SET t.id_player = u.id_player;
I'd put a random number column in your SQL selects for each row. Then you can order your record set by the random number directly in SQL.
Do this for both players and territory lists.
This example should work for any number of territories and players. If the territories don't divide up equally, it will be at random which players get one extra territory.
We are going to loop through the territories list once, in random order. We are going to step through the player list until we reach the end, then reload the player list again. This happens as many times as needed.
Might need to fix this for syntax or logic. I don't have an Oracle instance to test it.
CREATE OR REPLACE PROCEDURE giveterritory (pID_GAME PLS_INTEGER) IS
CURSOR c_player IS
SELECT id_player, dbms_random.value() rnd
FROM players
WHERE id_game = pID_GAME
ORDER BY 2 desc;
--
CURSOR c_territory IS
SELECT id_territory, dbms_random.value() rnd
FROM territories
WHERE id_game = pID_GAME
FOR UPDATE of id_player
ORDER BY 2 desc;
--
l_player_row c_player%ROWTYPE;
l_territory_row c_territory%ROWTYPE;
BEGIN
OPEN c_player;
FETCH c_player into l_player_row;
--
FOR r in c_territory
LOOP
UPDATE territories
SET id_player = l_player_row.id_player
WHERE CURRENT OF c_territory;
--
FETCH c_player INTO l_player_row;
IF c_player%NOTFOUND THEN
CLOSE c_player;
OPEN c_player;
FETCH c_player INTO l_player_row;
END IF;
END LOOP;
END;

How can I update a table based on a plpgsql instruction loop on PostgreSQL 8.3.16?

I got a problem I not sure how to solve it so far...
I have two tables that are related to each other with a 1 x n relation. I will try to describe the more importants fields below:
Table One - company:id PK,companyname varchar;
Table Two - training: course varchar,companyid bigint FK,id PK;
The problem is: I would like to update the information on course field of the table training because there are many courses with the same name. My idea is use something like
for s in 1..n loop
update training set course = course || s;
end loop;
No need for a loop you can do this with plain SQL:
with numbered as (
select id,
row_number() over (order by id) as rn
from training
)
update training
set course = course||n.rn::text
from numbered n
where n.id = training.id;
The common table expression assigns a number for each row in the training table and that number is then used to generate the new course name.
UPDATE training
SET course = course || num
FROM (
SELECT generate_series(1, (
SELECT count(course)
FROM training
)) num
) t
I solved mine doubt creating this function below:
CREATE OR REPLACE FUNCTION changeName()
RETURNS VOID AS
$$
DECLARE
table1id table1.id%TYPE;
counter1 RECORD;
counter2 RECORD;
BEGIN
FOR counter1 IN SELECT repeated_column,foreign_keyid,COUNT(repeated_column) AS contagem
FROM table1 GROUP BY repeated_column,foreign_keyid HAVING COUNT(repeated_column) > 1 LOOP
FOR counter2 IN 1..counter1.contagem LOOP
SELECT id INTO table1id FROM table1 WHERE repeated_column IN(counter1.repeated_column) AND foreign_keyid = counter1.foreign_keyid;
UPDATE table1 SET repeated_column = repeated_column || ' (' || counter2 || ')' WHERE id = table1id;
END LOOP;
END LOOP;
END;
$$
LANGUAGE 'plpgsql'

PL/SQl Error. Can't figure it out

I have tried to execute code the instruction below. But somehow I cant get it working. I am new to PL/SQl. Any hint will be valuable thanks
The rank table has:
rankID Number
name Varchar2(255 BYTE)
/*
Write PL/SQL program (anonymous block) that prints out a list of all ranks (ID
and name) for all rank ID from 100 to 110. If a rank ID (xxx) does not appear
in the rank table the program should print out: NO RANK AVAILABLE for ID:
xxx
*/
--set serveroutput on
DECLARE
rank_id NUMBER;
rank_name VARCHAR2(255);
loopcount NUMBER;
BEGIN
loopcount :=100;
FOR k IN 100..110
LOOP
SELECT rankID, name
INTO rank_id, rank_name
FROM rank
WHERE rankID=loopcount;
DBMS_OUTPUT.PUT_LINE(rank_id||' '|| rank_name);
loopcount := loopcount + 1;
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('NO RANK AVAILABLE for ID: '||rank_id);
END;
/
Below is what I am getting. Its not working the way it should
Server output:
100 Chefpilot
NO RANK AVAILABLE for ID: 100
DECLARE executed successfully
Execution time: 0.26s
No real need for a loop in this case, you can do this with an outer join, a case statement and a numbers table:
WITH CTE (RankId) AS (
SELECT 100 RankId
FROM DUAL
UNION ALL
SELECT RankId + 1
FROM CTE
WHERE RankId < 110
)
SELECT t.RankId, COALESCE(r.Name, 'Does not exist') Name,
CASE
WHEN r.RankId IS NULL THEN 'No rank available for: ' || t.RankId
ELSE r.RankId || ' ' || r.Name
END Description
FROM CTE t
LEFT JOIN rank r ON t.RankId = r.RankId
ORDER BY t.RankId
SQL Fiddle Demo
#sgeddes's approach is better, but to explain what you are seeing, if you did want to use your mechamism then you'd need to catch the exception in an inner block. At the moment the exception handler is outside the loop, so the first error you see terminates the loop. With an inner block:
DECLARE
rank_id NUMBER;
rank_name VARCHAR2(255);
BEGIN
FOR loopID IN 100..110
LOOP
BEGIN -- inner block
SELECT rankID, name
INTO rank_id, rank_name
FROM rank
WHERE rankID=loopID;
DBMS_OUTPUT.PUT_LINE(rank_id||' '|| rank_name);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('NO RANK AVAILABLE for ID: '||loopID);
END; -- inner block
END LOOP;
END;
/
Now if the ID doesn't exist the exception is caught, the message is printed, and the inner block exits; which means the loop continues at the next iteration. (I've also removed the extra loopcount and consistently used a loopID for the for and both references).

SQL Query to get count of words in table

I have a table which have schema like this
id name
1 jack
2 jack of eden
3 eden of uk
4 m of s
I want to execute a query which gives me count of words like this
count word
2 jack
2 eden
3 of
this means jack has been here 2 times, eden 2 times and of has been 3 times.
Hope you got the question, m trying too but not getting the right query or approach to it
thnx
Assuming your table is named temp (probably not - change it to the right name of your table)
I used a subquery for finding all the words in your table:
select distinct regexp_substr(t.name, '[^ ]+',1,level) word , t.name, t.id
from temp t
connect by level <= regexp_count(t.name, ' ') + 1
this query splits all the words from all records. I aliased it words.
Then I joined it with your table (in the query it's called temp) and counted the number of occurences in every record.
select words.word, count(regexp_count(tt.name, words.word))
from(
select distinct regexp_substr(t.name, '[^ ]+',1,level) word , t.name, t.id
from temp t
connect by level <= regexp_count(t.name, ' ') + 1) words, temp tt
where words.id= tt.id
group by words.word
You can also add:
having count(regexp_count(tt.name, words.word)) > 1
update: for better performance we can replace the inner subquery with the results of a pipelined function:
first, create a schema type and a table of it:
create or replace type t is object(word varchar2(100), pk number);
/
create or replace type t_tab as table of t;
/
then create the function:
create or replace function split_string(del in varchar2) return t_tab
pipelined is
word varchar2(4000);
str_t varchar2(4000) ;
v_del_i number;
iid number;
cursor c is
select * from temp; -- change to your table
begin
for r in c loop
str_t := r.name;
iid := r.id;
while str_t is not null loop
v_del_i := instr(str_t, del, 1, 1);
if v_del_i = 0 then
word := str_t;
str_t := '';
else
word := substr(str_t, 1, v_del_i - 1);
str_t := substr(str_t, v_del_i + 1);
end if;
pipe row(t(word, iid));
end loop;
end loop;
return;
end split_string;
now the query should look like:
select words.word, count(regexp_count(tt.name, words.word))
from(
select word, pk as id from table(split_string(' '))) words, temp tt
where words.id= tt.id
group by words.word

How can I combine multiple rows into a comma-delimited list in Oracle? [duplicate]

This question already has answers here:
SQL Query to concatenate column values from multiple rows in Oracle
(10 answers)
Closed 8 years ago.
I have a simple query:
select * from countries
with the following results:
country_name
------------
Albania
Andorra
Antigua
.....
I would like to return the results in one row, so like this:
Albania, Andorra, Antigua, ...
Of course, I can write a PL/SQL function to do the job (I already did in Oracle 10g), but is there a nicer, preferably non-Oracle-specific solution (or may be a built-in function) for this task?
I would generally use it to avoid multiple rows in a sub-query, so if a person has more then one citizenship, I do not want her/him to be a duplicate in the list.
My question is based on the similar question on SQL server 2005.
UPDATE:
My function looks like this:
CREATE OR REPLACE FUNCTION APPEND_FIELD (sqlstr in varchar2, sep in varchar2 ) return varchar2 is
ret varchar2(4000) := '';
TYPE cur_typ IS REF CURSOR;
rec cur_typ;
field varchar2(4000);
begin
OPEN rec FOR sqlstr;
LOOP
FETCH rec INTO field;
EXIT WHEN rec%NOTFOUND;
ret := ret || field || sep;
END LOOP;
if length(ret) = 0 then
RETURN '';
else
RETURN substr(ret,1,length(ret)-length(sep));
end if;
end;
The WM_CONCAT function (if included in your database, pre Oracle 11.2) or LISTAGG (starting Oracle 11.2) should do the trick nicely. For example, this gets a comma-delimited list of the table names in your schema:
select listagg(table_name, ', ') within group (order by table_name)
from user_tables;
or
select wm_concat(table_name)
from user_tables;
More details/options
Link to documentation
Here is a simple way without stragg or creating a function.
create table countries ( country_name varchar2 (100));
insert into countries values ('Albania');
insert into countries values ('Andorra');
insert into countries values ('Antigua');
SELECT SUBSTR (SYS_CONNECT_BY_PATH (country_name , ','), 2) csv
FROM (SELECT country_name , ROW_NUMBER () OVER (ORDER BY country_name ) rn,
COUNT (*) OVER () cnt
FROM countries)
WHERE rn = cnt
START WITH rn = 1
CONNECT BY rn = PRIOR rn + 1;
CSV
--------------------------
Albania,Andorra,Antigua
1 row selected.
As others have mentioned, if you are on 11g R2 or greater, you can now use listagg which is much simpler.
select listagg(country_name,', ') within group(order by country_name) csv
from countries;
CSV
--------------------------
Albania, Andorra, Antigua
1 row selected.
For Oracle you can use LISTAGG
You can use this as well:
SELECT RTRIM (
XMLAGG (XMLELEMENT (e, country_name || ',')).EXTRACT ('//text()'),
',')
country_name
FROM countries;
you can try this query.
select listagg(country_name,',') within group (order by country_name) cnt
from countries;
The fastest way it is to use the Oracle collect function.
You can also do this:
select *
2 from (
3 select deptno,
4 case when row_number() over (partition by deptno order by ename)=1
5 then stragg(ename) over
6 (partition by deptno
7 order by ename
8 rows between unbounded preceding
9 and unbounded following)
10 end enames
11 from emp
12 )
13 where enames is not null
Visit the site ask tom and search on 'stragg' or 'string concatenation' . Lots of
examples. There is also a not-documented oracle function to achieve your needs.
I needed a similar thing and found the following solution.
select RTRIM(XMLAGG(XMLELEMENT(e,country_name || ',')).EXTRACT('//text()'),',') country_name from
In this example we are creating a function to bring a comma delineated list of distinct line level AP invoice hold reasons into one field for header level query:
FUNCTION getHoldReasonsByInvoiceId (p_InvoiceId IN NUMBER) RETURN VARCHAR2
IS
v_HoldReasons VARCHAR2 (1000);
v_Count NUMBER := 0;
CURSOR v_HoldsCusror (p2_InvoiceId IN NUMBER)
IS
SELECT DISTINCT hold_reason
FROM ap.AP_HOLDS_ALL APH
WHERE status_flag NOT IN ('R') AND invoice_id = p2_InvoiceId;
BEGIN
v_HoldReasons := ' ';
FOR rHR IN v_HoldsCusror (p_InvoiceId)
LOOP
v_Count := v_COunt + 1;
IF (v_Count = 1)
THEN
v_HoldReasons := rHR.hold_reason;
ELSE
v_HoldReasons := v_HoldReasons || ', ' || rHR.hold_reason;
END IF;
END LOOP;
RETURN v_HoldReasons;
END;
I have always had to write some PL/SQL for this or I just concatenate a ',' to the field and copy into an editor and remove the CR from the list giving me the single line.
That is,
select country_name||', ' country from countries
A little bit long winded both ways.
If you look at Ask Tom you will see loads of possible solutions but they all revert to type declarations and/or PL/SQL
Ask Tom
SELECT REPLACE(REPLACE
((SELECT TOP (100) PERCENT country_name + ', ' AS CountryName
FROM country_name
ORDER BY country_name FOR XML PATH('')),
'&<CountryName>', ''), '&<CountryName>', '') AS CountryNames
you can use this query to do the above task
DECLARE #test NVARCHAR(max)
SELECT #test = COALESCE(#test + ',', '') + field2 FROM #test SELECT field2= #test
for detail and step by step explanation visit the following link
http://oops-solution.blogspot.com/2011/11/sql-server-convert-table-column-data.html