PostgreSQL Query with dynamic columns and counts from join - postgresql

I'm probably overthinking this, because I'm not really sure where to start... But here goes:
I have the following
Tables
students
assessments
students_assessments
Expected output
First query for "Attempts"
student_id | Assessment 1 | Assessment 2 | Assessment 3 | Assessment 4
1 3 1 2 0
2 1 0 0 0
3 2 1 1 0
4 5 3 3 0
5 1 5 0 0
6 2 1 2 0
Second query for "passed"
student_id | Assessment 1 | Assessment 2 | Assessment 3 | Assessment 4
1 t t f f
2 t t f f
3 t t f f
4 t t t f
5 t t f f
6 t t t f
The part that is tripping me up is not doing a join for EVERY assessment, and not even defining the columns manually but rather "generating" each column for assessments that exist.
I feel like it's simple, and I am just too overworked to figure it out right now :) thank you in advance for your help, and here's a SQL Fiddle of data as an example

Simple query for "Attempts",
select student_id,sum(case when assessment_id=1 then 1 else 0 end) as "Assessment 1",
sum(case when assessment_id=2 then 1 else 0 end) as "Assessment 2",
sum(case when assessment_id=3 then 1 else 0 end) as "Assessment 3",
sum(case when assessment_id=4 then 1 else 0 end) as "Assessment 4",
sum(case when assessment_id=5 then 1 else 0 end) as "Assessment 5",
sum(case when assessment_id=6 then 1 else 0 end) as "Assessment 6"
from assessments_students
group by student_id
order by student_id
In crosstab() function also, need to define columns name explicitly like "Assessment 1","Assessment 2" and so on.
Or write custom function for creating dynamic query, and execute using EXECUTE statement.
DROP FUNCTION get_Attempts() ;
CREATE OR REPLACE FUNCTION get_Attempts() RETURNS text AS
$BODY$
DECLARE
r1 record;
str_query text := '';
BEGIN
str_query :='select student_id,';
FOR r1 IN SELECT "_id" , "name" FROM Assessments
LOOP
str_query:= str_query ||
'sum(case when assessment_id=' || r1."_id" || ' then 1 else 0 end) as "' || r1.name ||'",' ;
END LOOP;
str_query:=trim( trailing ',' from str_query); -- remove last semicolon
str_query:= str_query || ' from assessments_students group by student_id order by student_id';
return str_query;
END
$BODY$
LANGUAGE 'plpgsql' ;
SELECT * FROM get_Attempts();
Second query for "passed"
select student_id,
max(case when assessment_id=1 and passed='t' then 't' else 'f' end) as "Assessment 1",
max(case when assessment_id=2 and passed='t' then 't' else 'f' end) as "Assessment 2",
max(case when assessment_id=3 and passed='t' then 't' else 'f' end) as "Assessment 3",
max(case when assessment_id=4 and passed='t' then 't' else 'f' end) as "Assessment 4",
max(case when assessment_id=5 and passed='t' then 't' else 'f' end) as "Assessment 5",
max(case when assessment_id=6 and passed='t' then 't' else 'f' end) as "Assessment 6"
from assessments_students
group by student_id
order by student_id
and it's function look likes,
DROP FUNCTION get_passed() ;
CREATE OR REPLACE FUNCTION get_passed() RETURNS text AS
$BODY$
DECLARE
r1 record;
str_query text := '';
BEGIN
str_query :='select student_id,';
FOR r1 IN SELECT "_id" , "name" FROM Assessments
LOOP
str_query:= str_query ||
'max(case when assessment_id=' || r1."_id" || ' and passed=''t'' then ''t'' else ''f'' end) as "' || r1.name ||'",' ;
END LOOP;
str_query:=trim( trailing ',' from str_query); -- remove last semicolon
str_query:= str_query || ' from assessments_students group by student_id order by student_id';
return str_query;
END
$BODY$
LANGUAGE 'plpgsql' ;
SELECT * FROM get_passed();

SELECT * FROM crosstab(
'Select studentid, assessmentname,count (studentassessmentid) from ....[do ur joins here] group by studentid,assessmentname order by 1,2' AS ct (studentid, assesment1, assessment2,assesent3,assessment4);

I realized it doesn't work anymore. I'm using PostgreSQL 12.
So here you cannot return non-predefined table type (I mean variable columns) either select from varying char.
An example with using anonymous code block, crosstab and temp tables.
Cursors are redundand, I was solving a task that meant of using them.
CREATE EXTENSION IF NOT EXISTS tablefunc;
DO
$$
DECLARE
movie_probe CURSOR FOR
SELECT m.name movie_name, count(c.id) cinema_count
FROM movies m
JOIN sessions s ON m.id = s.movie_id
JOIN cinema_halls ch ON s.cinema_hall_id = ch.id
JOIN cinemas c ON ch.cinema_id = c.id
GROUP BY m.name
HAVING count(c.name) > 1
ORDER BY count(c.name) DESC;
movie_rec RECORD;
movie_columns TEXT DEFAULT '';
cinemas_probe CURSOR (cond_movie_name TEXT) FOR
SELECT m.name movie_name, c.name cinema_name
FROM movies m
JOIN sessions s ON m.id = s.movie_id
JOIN cinema_halls ch ON s.cinema_hall_id = ch.id
JOIN cinemas c ON ch.cinema_id = c.id
WHERE cond_movie_name = m.name
ORDER BY c.name;
cinema_rec RECORD;
cinema_row_counter INT DEFAULT 0;
BEGIN
DROP TABLE IF EXISTS cinema_multiples_aev;
CREATE TEMP TABLE cinema_multiples_aev (
row_id INT,
movie_name TEXT,
cinema_name TEXT
);
OPEN movie_probe;
LOOP
FETCH movie_probe INTO movie_rec;
EXIT WHEN NOT FOUND;
OPEN cinemas_probe(movie_rec.movie_name);
LOOP
FETCH cinemas_probe INTO cinema_rec;
EXIT WHEN NOT FOUND;
cinema_row_counter := cinema_row_counter + 1;
INSERT INTO cinema_multiples_aev (row_id, movie_name, cinema_name)
VALUES (cinema_row_counter, cinema_rec.movie_name, cinema_rec.cinema_name);
END LOOP;
CLOSE cinemas_probe;
cinema_row_counter := 0;
movie_columns := movie_columns || ', "' || movie_rec.movie_name || '" TEXT';
END LOOP;
CLOSE movie_probe;
movie_columns := substring(movie_columns FROM 2);
DROP TABLE IF EXISTS movie_multiples;
EXECUTE format('CREATE TEMP TABLE movie_multiples(row_id INT, %s)', movie_columns);
EXECUTE format(E'
INSERT INTO movie_multiples
SELECT *
FROM crosstab(\'select row_id, movie_name, cinema_name
from cinema_multiples_aev
order by 1,2\')
AS cinema_multiples_aev(row_id INT, %s);
', movie_columns, movie_columns);
ALTER TABLE movie_multiples DROP COLUMN row_id;
END
$$ LANGUAGE plpgsql;
SELECT *
FROM movie_multiples;
DROP TABLE IF EXISTS movie_multiples;
DROP TABLE IF EXISTS cinema_multiples_aev;
If it's confusing for the lack of strusture, everything could be found here github

Related

Count null values in each column of a table : PSQL

I have a very big table but as an example I will only provide a very small part of it as following:-
col1 col2 col3 col4
10 2 12
13 4 11
0 1
3 5 111
I know how to find null values in one column. What I want to find is how many null values are there in each column just by writing one query.
Thanks in advance
You can use an aggregate with a filter:
select count(*) filter (where col1 is null) as col1_nulls,
count(*) filter (where col2 is null) as col2_nulls,
count(*) filter (where col3 is null) as col3_nulls,
count(*) filter (where col4 is null) as col4_nulls
from the_table;
I think you can generate this query on the fly. Here is an example of one way you can approach it:
CREATE OR REPLACE FUNCTION null_counts(tablename text)
RETURNS SETOF jsonb LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE 'SELECT to_jsonb(t) FROM (SELECT ' || (
SELECT string_agg('count(*) filter (where ' || a.attname::text || ' is null) as ' || a.attname || '_nulls', ',')
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = tablename::regclass
AND a.attnum > 0
AND a.attisdropped = false
) || ' FROM ' || tablename::regclass || ') as t';
END
$func$;
SELECT null_counts('your_table') AS val;

db2 - Update statement using subselect with case

I want to do an update statement according to a result of a subquery
For example :
Update TABLE1
set A= (Select Count(*) from TABLE2 )
if the value of count is 0 then Update the value of A to be 0 Else set A = 1;
So could you please advice me how can I do it?
I tried the following but I got a syntax error :
SELECT count(*) as TC
CASE
WHEN TC > 0
THEN '1'
ELSE '0'
END AS dum
FROM Event E where E.Type= 'CANCELLING';
CASE is perfectly suitable:
UPDATE TABLE1
SET A =
CASE
WHEN (SELECT count(*) FROM TABLE2) > 0 THEN 1
ELSE 0
END
declare #count int
set #count=Select Count(*) from TABLE2
if #count=0
BEGIN
Update TABLE1
set A=0
end
else
Update TABLE1
set A=1

Using parameters of Stored Procedure with Join's "ON" area

I have parameters like these
declare #Phl1_descr varchar(50)
SET #Phl1_descr = 'Greece'
declare #Phl2_descr varchar(50)
SET #Phl2_descr = 'Coffee & Beverages'
I want to join two tables with the above parameters (if they are not null), so I tried to do something like below in the "ON" keyword of my JOIN
ON
(CASE WHEN LEN(#Phl1_descr) > 0 THEN A.Phl1_descr ELSE B.Phl1_descr END) = B.Phl1_descr AND
(CASE WHEN LEN(#Phl2_descr) > 0 THEN A.Phl2_descr ELSE B.Phl2_descr END) = B.Phl2_descr
However if I send one of the parameters like as '', it doesn't work. Any simpler idea?
Is it posible to use simpler solution? Like:
IF #Phl1_descr IS NOT NULL AND #Phl2_descr IS NOT NULL
BEGIN
SELECT *
FROM Table1 as A
LEFT JOIN Table2 as B on A.Phl1_descr=B.Phl1_descr and A.Phl2_descr=B.Phl2_descr
END
ELSE IF #Phl1_descr IS NOT NULL AND #Phl2_descr IS NULL
BEGIN
SELECT *
FROM Table1 as A
LEFT JOIN Table2 as B on A.Phl1_descr=B.Phl1_descr
END
ELSE IF #Phl1_descr IS NULL AND #Phl2_descr IS NOT NULL
BEGIN
SELECT *
FROM Table1 as A
LEFT JOIN Table2 as B on A.Phl2_descr=B.Phl2_descr
END
So you will get a simpler execution plans and simpler logic.
You can also use ... CASE WHEN #Phl1_descr IS NULL THEN ... to check NULL values
Interesting but
B.Phl1_descr = B.Phl1_descr
not working but
ISNULL(B.Phl1_descr,'-1') = ISNULL(B.Phl1_descr,'-1')
works,
So just a simple change in the below code work it out
(CASE WHEN LEN(#Phl1_descr) > 1 THEN A.Phl1_descr ELSE ISNULL(B.Phl1_descr,'-1') END) = ISNULL(B.Phl1_descr,'-1') AND
(CASE WHEN LEN(#Phl2_descr) > 1 THEN A.Phl2_descr ELSE ISNULL(B.Phl2_descr,'-1') END) = ISNULL(B.Phl2_descr,'-1') AND

SQL Running Subtraction and Deviation

-- Just a brief of business scenario is table has been created for a good receipt.
-- So here we have good expected line with PurchaseOrder(PO) in first few line.
-- And then we receive each expected line physically and that time these quantity may be different
-- due to business case like quantity may damage and short quantity like that.
-- So we maintain a status for that eg: OK, Damage, also we have to calculate short quantity
-- based on total of expected quantity of each item and total of received line.
if object_id('DEV..Temp','U') is not null
drop table Temp
CREATE TABLE Temp
(
ID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
Item VARCHAR(32),
PO VARCHAR(32) NULL,
ExpectedQty INT NULL,
ReceivedQty INT NULL,
[STATUS] VARCHAR(32) NULL,
BoxName VARCHAR(32) NULL
)
-- Please see first few line with PO data will be the expected lines,
-- and then rest line will be received line
INSERT INTO TEMP (Item,PO,ExpectedQty,ReceivedQty,[STATUS],BoxName)
SELECT 'ITEM01','PO-01','30',NULL,NULL,NULL UNION ALL
SELECT 'ITEM01','PO-02','20',NULL,NULL,NULL UNION ALL
SELECT 'ITEM02','PO-01','40',NULL,NULL,NULL UNION ALL
SELECT 'ITEM03','PO-01','50',NULL,NULL,NULL UNION ALL
SELECT 'ITEM03','PO-02','30',NULL,NULL,NULL UNION ALL
SELECT 'ITEM03','PO-03','20',NULL,NULL,NULL UNION ALL
SELECT 'ITEM04','PO-01','30',NULL,NULL,NULL UNION ALL
SELECT 'ITEM01',NULL,NULL,'20','OK','box01' UNION ALL
SELECT 'ITEM01',NULL,NULL,'25','OK','box02' UNION ALL
SELECT 'ITEM01',NULL,NULL,'5','DAMAGE','box03' UNION ALL
SELECT 'ITEM02',NULL,NULL,'38','OK','box04' UNION ALL
SELECT 'ITEM02',NULL,NULL,'2','DAMAGE','box05' UNION ALL
SELECT 'ITEM03',NULL,NULL,'30','OK','box06' UNION ALL
SELECT 'ITEM03',NULL,NULL,'30','OK','box07' UNION ALL
SELECT 'ITEM03',NULL,NULL,'10','DAMAGE','box09' UNION ALL
SELECT 'ITEM04',NULL,NULL,'25','OK','box10'
-- Below Table is my expected result based on above data.
-- I need to show those data following way.
-- So I appreciate if you can give me an appropriate query for it.
-- Note: first row is blank and it is actually my table header. :)
-- Conditions : any of row, we cant have ReceivedQty, DamageQty and ShortQty
-- values more than ExpectedQty value. Item03 has this scenario
-- Query should run in SQL 2000 DB
SELECT ''as'ITEM', ''as'PO#', ''as'ExpectedQty',''as'ReceivedQty',''as'DamageQty' ,''as'ShortQty' UNION ALL
SELECT 'ITEM01','PO-01','30','30','0' ,'0' UNION ALL
SELECT 'ITEM01','PO-02','20','15','5' ,'0' UNION ALL
SELECT 'ITEM02','PO-01','40','38','2' ,'0' UNION ALL
SELECT 'ITEM03','PO-01','50','50','0' ,'0' UNION ALL
SELECT 'ITEM03','PO-02','30','20','10' ,'10' UNION ALL
SELECT 'ITEM03','PO-03','20','0','0','20' UNION ALL
SELECT 'ITEM04','PO-01','30','25','0' ,'5'
Using this solution as a starting point, I've eventually ended up with this:
SELECT
Item,
PO,
ExpectedQty,
ReceivedQty = CASE
WHEN RemainderQty >= 0 THEN ExpectedQty
WHEN RemainderQty < -ExpectedQty THEN 0
ELSE RemainderQty + ExpectedQty
END,
DamageQty = CASE
WHEN RemainderQty >=0 OR ExpectedQty < -TotalRemainderQty THEN 0
WHEN RemainderQty < -ExpectedQty AND TotalRemainderQty > 0 THEN ExpectedQty
WHEN RemainderQty < -ExpectedQty AND TotalRemainderQty < -DamagedQty THEN ExpectedQty + TotalRemainderQty
WHEN RemainderQty > -DamagedQty THEN -RemainderQty
ELSE DamagedQty
END,
ShortQty = CASE
WHEN TotalRemainderQty >= 0 THEN 0
WHEN TotalRemainderQty < -ExpectedQty THEN ExpectedQty
ELSE -TotalRemainderQty
END
FROM (
SELECT
a.Item,
a.PO,
a.ExpectedQty,
b.DamagedQty,
RemainderQty = b.ReceivedQty - a.RunningTotalQty,
TotalRemainderQty = b.ReceivedQty + b.DamagedQty - a.RunningTotalQty
FROM (
SELECT
a.Item,
a.PO,
a.ExpectedQty,
RunningTotalQty = SUM(a2.ExpectedQty)
FROM (SELECT Item, PO, ExpectedQty FROM Temp WHERE STATUS IS NULL) AS a
INNER JOIN (SELECT Item, PO, ExpectedQty FROM Temp WHERE STATUS IS NULL) AS a2
ON a.Item = a2.Item AND a.PO >= a2.PO
GROUP BY
a.Item,
a.PO,
a.ExpectedQty
) a
LEFT JOIN (
SELECT
Item,
ReceivedQty = SUM(CASE STATUS WHEN 'OK' THEN ReceivedQty ELSE 0 END),
DamagedQty = SUM(CASE STATUS WHEN 'DAMAGE' THEN ReceivedQty ELSE 0 END)
FROM Temp
GROUP BY Item
) b ON a.Item = b.Item
) s;

Implementing and applying a string split in T-SQL

I have this statement in T-SQL.
SELECT Bay From TABLE where uid in (
select B_Numbers from Info_Step WHERE uid = 'number'
)
I am selecting "multiple" BAYs from TABLE where their uid is equal to a string of numbers like this:
B_Numbers = 1:45:34:98
Therefore, I should be selecting 4 different BAYs from TABLE. I basically need to split the string 1:45:34:98 up into 4 different numbers.
I'm thinking that Split() would work, but it doesn't and I get a syntax error.
Any thoughts from the T-SQL gods would be awesome!
Here is an implementation of a split function that returns the list of numbers as a table:
http://rbgupta.blogspot.com/2007/03/split-function-tsql.html
Looks like this would set you on your way...
Here is a method that uses an auxiliary numbers table to parse the input string. The logic can easily be added to a function that returns a table. That table can then be joined to lookup the correct rows.
Step 1: Create the Numbers table
SET NOCOUNT ON
GO
IF EXISTS
(
SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'Numbers'
AND TABLE_SCHEMA = 'dbo'
AND TABLE_TYPE = 'BASE TABLE'
)
BEGIN
DROP TABLE dbo.Numbers
END
GO
CREATE TABLE dbo.Numbers
(
Number smallint IDENTITY(1, 1) PRIMARY KEY
)
GO
WHILE 1 = 1
BEGIN
INSERT INTO dbo.Numbers DEFAULT VALUES
IF SCOPE_IDENTITY() = 32767
BEGIN
BREAK
END
END
GO
Step 2: Parse the Input String
CREATE FUNCTION dbo.ParseString(#input_string varchar(8000), #delim varchar(8000) = " ")
RETURNS TABLE
AS RETURN
(
SELECT Number
FROM dbo.Numbers
WHERE CHARINDEX
(
#delim + CONVERT(VARCHAR(12),Number) + #delim,
#delim + #input_string + #delim
) > 0
)
GO
**EXAMPLE**
SELECT * FROM dbo.ParseString('1:45:34:98',':')
Step 3: Use the results however you want/need
Number
------
1
34
45
98
End-To-End Example
Create function that returns the appropriate BNumber (of course change it to use the commented out SQL)
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION dbo.GetBNumber (#uid int)
RETURNS VARCHAR(8000)
AS
BEGIN
RETURN '1:45:34:98'
--select B_Numbers from Info_Step WHERE uid = #uid
END
GO
Use the use functions to return the desired results
-- Using Test Data
SELECT N.Number FROM Numbers N
JOIN dbo.ParseString(dbo.GetBNumber(12345),':') Q ON Q.Number = N.Number
-- Using Your Data (Untested but should work.)
SELECT N.Bay
FROM TABLE N
JOIN dbo.ParseString(dbo.GetBNumber(ENTER YOU NUMBER HERE),':') Q ON Q.Number = N.uid
Results
Number
------
1
34
45
98
You should keep your arrays as rows but if I understand your question I think this will work.
SELECT
Bay
From
TABLE
join Info_Step
on B_Numbers like '%'+ uid +'%'
where
Info_Step.uid = 'number'
This query will do a full table scan because of the like operator.
What you can do is loop through the B_Numbers entries and do your own split on : Insert those entries into a temp table and then perform your query.
DECLARE #i int
DECLARE #start int
DECLARE #B_Numbers nvarchar(20)
DECLARE #temp table (
number nvarchar(10)
)
-- SELECT B_Numbers FROM Info_Step WHERE uid = 'number'
SELECT #B_Numbers = '1:45:34:98'
SET #i = 0
SET #start = 0
-- Parse out characters delimited by ":";
-- Would make a nice user defined function.
WHILE #i < len(#B_Numbers)
BEGIN
IF substring(#B_Numbers, #i, 1) = ':'
BEGIN
INSERT INTO #temp
VALUES (substring(#B_Numbers, #start, #i - #start))
SET #start = #i + 1
END
SET #i = #i + 1
END
-- Insert last item
INSERT INTO #temp
VALUES (substring(#B_Numbers, #start, #i - #start + 1))
-- Do query with parsed values
SELECT Bay FROM TABLE WHERE uid in (SELECT * FROM #temp)
You can even try this
declare #str varchar(50)
set #str = '1:45:34:98'
;with numcte as(
select 1 as rn union all select rn+1 from numcte where rn<LEN(#str)),
getchars as(select
ROW_NUMBER() over(order by rn) slno,
rn,chars from numcte
cross apply(select SUBSTRING(#str,rn,1) chars)X where chars = ':')
select top 1
Bay1 = SUBSTRING(#str,0,(select rn from getchars where slno = 1))
,Bay2 = SUBSTRING(#str,
(select rn from getchars where slno = 1) + 1,
(((select rn from getchars where slno = 2)-
(select rn from getchars where slno = 1)
)-1))
,Bay3 = SUBSTRING(#str,
(select rn from getchars where slno = 2) + 1,
(((select rn from getchars where slno = 3)-
(select rn from getchars where slno = 2)
)-1))
,Bay4 = SUBSTRING(#str,
(select rn from getchars where slno = 3)+1,
LEN(#str))
from getchars
Output:
Bay1 Bay2 Bay3 Bay4
1 45 34 98