PostgreSQL: how to convert "PIVOT" from T-SQL to PLPGSQL procedure - postgresql

I would like to convert a stored procedure from T-SQL to PL/PGSQL.
Here is the T-SQL code
SELECT NO_PTF, coalesce(1, 0) as RISK_1, coalesce(2, 0) as RISK_2, coalesce(3, 0) as RISK_3, coalesce(4, 0) as RISK_4
FROM (
SELECT D.NO_PTF, C.NB_DEG_RSQ, D.MT_DEM/100 as MT_DEM
FROM public.TB_Demande D
INNER JOIN
public.TB_Compartiment C ON D.ID_CPA = C.ID_CPA
AND D.MC_UTL = 'F3000'
AND C.IN_ACT = true
) Q
PIVOT (SUM(Q.MT_DEM) FOR Q.NB_DEG_RSQ IN (1, 2, 3, 4)) as PVT
)
I tried to replace PIVOT by crosstab. But i have an error. Here is my PL/PGSQL code
SELECT NO_PTF, coalesce(1, 0) as RISK_1, coalesce(2, 0) as RISK_2, coalesce(3, 0) as RISK_3, coalesce(4, 0) as RISK_4
FROM (
SELECT D.NO_PTF, C.NB_DEG_RSQ, D.MT_DEM/100 as MT_DEM
FROM public.TB_Demande D
INNER JOIN
public.TB_Compartiment C ON D.ID_CPA = C.ID_CPA
AND D.MC_UTL = 'F3000'
AND C.IN_ACT = true
) Q
crosstab (SUM(Q.MT_DEM) FOR Q.NB_DEG_RSQ IN (1, 2, 3, 4)) as PVT
Someone have an idea about the rigth way to use crosstab here?

I am not entirely sure I understand the PIVOT syntax, but typically this is way easier in Postgres using a filtered aggregation, not the somewhat clumsy crosstab() function.
I think this should be equivalent:
select no_ptf,
sum(mt_dem) filter (where NB_DEG_RSQ = 1) as risk_1,
sum(mt_dem) filter (where NB_DEG_RSQ = 2) as risk_2,
sum(mt_dem) filter (where NB_DEG_RSQ = 3) as risk_3,
sum(mt_dem) filter (where NB_DEG_RSQ = 4) as risk_4
from (
SELECT D.NO_PTF, C.NB_DEG_RSQ, D.MT_DEM/100 as MT_DEM
FROM public.TB_Demande D
INNER JOIN public.TB_Compartiment C
ON D.ID_CPA = C.ID_CPA
AND D.MC_UTL = 'F3000'
AND C.IN_ACT = true
)
GROUP BY no_ptf;

Related

Syntax Error: Lateral Join with Delete (PostgreSQL)

PostgreSQL 11.1
AFAIK, this is correct and should run. It Fails with syntax error on Delete. What am I missing?
Thanks for any help.
ERROR: syntax error at or near "DELETE"
LINE 41: DELETE FROM d
WITH _in (tservice, patient_recid, disease_recid, new_disease_recid) AS (
VALUES ('2021-04-21'::timestamp, '23262'::integer, '34978'::integer, '33364'::integer)
)
UPDATE dx d
SET disease_recid = n.new_disease_recid
FROM _in n,
LATERAL ( WITH RECURSIVE readtoend AS(
SELECT recid, newrecid
FROM patients p1
JOIN _in n ON p1.recid = n.patient_recid
UNION
SELECT c.recid, c.newrecid
FROM patients c
INNER JOIN readtoend s ON s.newrecid = c.recid
),
readtostart AS(
SELECT recid, newrecid
FROM patients p1
JOIN _in n ON p1.recid = n.patient_recid
UNION
SELECT c.recid, c.newrecid
FROM patients c
INNER JOIN readtostart s ON s.recid = c.newrecid
)
SELECT recid FROM readtoend
UNION
SELECT recid FROM readtostart
) j,
LATERAL ( WITH _get_existing_target AS(
SELECT d.*
FROM d
WHERE (d.patient_recid, d.disease_recid) = (j.recid, n.new_disease_recid) AND d.tservice <= n.tservice
),
_get_conflicts AS(
SELECT d.*
FROM d
WHERE (d.patient_recid, d.disease_recid) = (j.recid, n.disease_recid) AND EXISTS ( SELECT 1
FROM _get_existing_target x
WHERE d.patient_recid = x.patient_recid AND d.tservice::date = x.tservice::date)
)
DELETE FROM d
USING _get_conflicts f
WHERE d.recid = f.recid
RETURNING d.*
) del
WHERE (d.patient_recid, d.disease_recid) = (j.recid, n.disease_recid) AND d.tservice::date <= n.tservice::date
AND d.recid NOT IN ( SELECT recid FROM del);
You cannot use DELETE ... RETURNING in the FROM list of a query.

How to append CTE results to main query output?

I've created a TSQL query that pulls from two sets of tables in my database. The tables in the Common Table Expression are different from the tables in the main query. I'm joining on MRN and need the end result to contain accounts from both sets of tables. I've written the following query to this end:
with cteHosp as(
select Distinct p.EncounterNumber, p.MRN, p.AdmitAge
from HospitalPatients p
inner join Eligibility e on p.MRN = e.MRN
inner join HospChgDtl c on p.pt_id = c.pt_id
inner join HospitalDiagnoses d on p.pt_id = d.pt_id
where p.AdmitAge >=12
and d.dx_cd in ('G89.4','R52.1','R52.2','Z00.129')
)
Select Distinct a.AccountNo, a.dob, DATEDIFF(yy, a.dob, GETDATE()) as Age
from RHCCPTDetail c
inner join RHCAppointments a on c.ClaimID = a.ClaimID
inner join Eligibility e on c.hl7Id = e.MRN
full outer join cteHosp on e.MRN = cteHosp.MRN
where DATEDIFF(yy, a.dob, getdate()) >= 12
and left(c.PriDiag,7) in ('G89.4','R52.1','R52.2', 'Z00.129')
or (
DATEDIFF(yy, a.dob, getdate()) >= 12
and LEFT(c.DiagCode2,7) in ('G89.4','R52.1','R52.2','Z00.129')
)
or (
DATEDIFF(yy, a.dob, getdate()) >= 12
and LEFT(c.DiagCode3,7) in ('G89.4','R52.1','R52.2','Z00.129')
)
or (
DATEDIFF(yy, a.dob, getdate()) >= 12
and LEFT(c.DiagCode4,7) in ('G89.4','R52.1','R52.2','Z00.129')
)
order by AccountNo
How do I merge together the output of both the common table expression and the main query into one set of results?
Merge performs inserts, updates or deletes. I believe you want to join the cte. If so, here is an example.
Notice the cteBatch is joined to the Main query below.
with
cteBatch (BatchID,BatchDate,Creator,LogID)
as
(
select
BatchID
,dateadd(day,right(BatchID,3) -1,
cast(cast(left(BatchID,4) as varchar(4))
+ '-01-01' as date)) BatchDate
,Creator
,LogID
from tblPriceMatrixBatch b
unpivot
(
LogID
for Logs in (LogIDISI,LogIDTG,LogIDWeb)
)u
)
Select
0 as isCurrent
,i.InterfaceID
,i.InterfaceName
,b.BatchID
,b.BatchDate
,case when isdate(l.start) = 0 and isdate(l.[end]) = 0 then 'Scheduled'
when isdate(l.start) = 1 and isdate(l.[end]) = 0 then 'Running'
when isdate(l.start) = 1 and isdate(l.[end]) = 1 and isnull(l.haserror,0) = 1 then 'Failed'
when isdate(l.start) = 1 and isdate(l.[end]) = 1 and isnull(l.haserror,0) != 1 then 'Success'
else 'idunno' end as stat
,l.Start as StartTime
,l.[end] as CompleteTime
,b.Creator as Usr
from EOCSupport.dbo.Interfaces i
join EOCSupport.dbo.Logs l
on i.InterfaceID = l.InterfaceID
join cteBatch b
on b.logid = l.LogID

T-SQL Populate Column Using While Loop

I have the following problem, and SQL isn't my strongest skill yet. I have the following procedure in SQL Server 2014 that works for returning the amount of money in each individual Quote for a Quoting System. Each Quote has Groups, which in turn have Parts in them.:
USE [My_DB_Name]
GO
DECLARE #QuoteNumberID int;
SELECT #QuoteNumberID = QuoteNumberId FROM [Quote].[Quote]
WHILE #QuoteNumberID IS NOT NULL
BEGIN
INSERT INTO [Quote].[ZQuoteBackupGL117] (QuoteAmount)(
SELECT ISNULL(SUM(ExtendedPrice) ,0) AS QuoteTotal
FROM(
SELECT (Quantity * ((UnitPrice - ISNULL(DollarDiscount, 0)) -
ROUND((((ISNULL(PercentDiscount,0)/100 + ISNULL(CustomerPercentDiscount,0)/100))) * UnitPrice, 2))) AS ExtendedPrice
FROM [Quote].[PartGroupPart] p
INNER JOIN [Quote].[QuotePartGroup] g ON p.PartGroupID = g.PartGroupID
INNER JOIN [Quote].[ZQuoteBackupGL117] q ON g.QuoteID = q.QuoteID
WHERE QuoteNumberId = #QuoteNumberID AND g.IsRecommended = 0 AND g.ExcludeFromTotal = 0 AND (q.GrandTotalValue IS NULL OR q.GrandTotalValue = 0)
)tmp)
SELECT #QuoteNumberID = MAX(QuoteNumberId) FROM [Quote].[Quote] WHERE #QuoteNumberId > QuoteNumberID
END
What I'm trying to do is get the value of QuoteTotal and put it in a new field in the [Quote].[ZQuoteBackupGL117] backup table named QuoteAmount(money, allow nulls) in each entry in the table. I will also run it on my production table later. Normally, I would avoid using a loop for this, but this query is only meant to run once to populate QuoteAmount for retroactive Quotes, of which there are thousands. I have looked into using INSERT INTO, but I'm not sure how I am supposed to structure it using this loop. Any help you guys can give me would be appreciated.
You probably want something like this.
UPDATE [Quote].[ZQuoteBackupGL117]
SET QuoteAmount = (
SELECT (Quantity * ((UnitPrice - ISNULL(DollarDiscount, 0)) - ROUND((((ISNULL(PercentDiscount,0)/100 + ISNULL(CustomerPercentDiscount,0)/100))) * UnitPrice, 2))) AS ExtendedPrice
FROM [Quote].[PartGroupPart] p
JOIN [Quote].[QuotePartGroup] g ON p.PartGroupID = g.PartGroupID
JOIN [Quote].[ZQuoteBackupGL117] q ON g.QuoteID = q.QuoteID
WHERE QuoteNumberId = #QuoteNumberID AND g.IsRecommended = 0 AND g.ExcludeFromTotal = 0 AND COALESCE(q.GrandTotalValue,0) = 0
)
WHERE QuoteNumberId = #QuoteNumberID
but I wouldn't bet any money on it.

How do I get the max from a partitioned query using Rank ()

I want the result to return the max Rank when partitioned using the rank function.
I am using the following query.
SELECT DISTINCT dbo.pomst.co_num
,dbo.pomst.wh_num
,dbo.pomst.po_number
,dbo.pomst.po_suffix
,dbo.pomst.vendor_id
,dbo.item.uom
,dbo.item.upc_num
,dbo.item.item_desc
,RIGHT(dbo.auditlog.pallet_id, 8) AS pallet_id
,dbo.auditlog.abs_num
,dbo.auditlog.item_qty
,dbo.auditlog.lot
,dbo.auditlog.packer
,auditlog.comments
,auditlog.date_time
,rank() OVER (
PARTITION BY auditlog.comments ORDER BY auditlog.date_time ASC
) AS CorrectTrans
FROM dbo.auditlog
INNER JOIN dbo.pomst ON dbo.auditlog.co_num = dbo.pomst.co_num
AND dbo.auditlog.wh_num = dbo.pomst.wh_num
AND dbo.auditlog.po_number = dbo.pomst.po_number
AND dbo.auditlog.po_suffix = dbo.pomst.po_suffix
INNER JOIN dbo.item ON dbo.auditlog.co_num = dbo.item.co_num
AND dbo.auditlog.wh_num = dbo.item.wh_num
AND dbo.auditlog.abs_num = dbo.item.abs_num
WHERE (dbo.pomst.co_num = 'AC01')
AND (dbo.pomst.wh_num = 'KU22')
AND (dbo.pomst.row_status = 'C')
AND (dbo.auditlog.trans_type = 're')
AND item_qty NOT LIKE '-%'
I figured it out! I was trying to get the max result of a rank, but if I flip order of rank from asc to desc and use a CTE I can select the the results that always have 1 as the rank as opposed to trying to get the Max. I would still like to know how to get the Max rank but this solution suits my needs.
;with cte as
(SELECT DISTINCT dbo.pomst.co_num
,dbo.pomst.wh_num
,dbo.pomst.po_number
,dbo.pomst.po_suffix
,dbo.pomst.vendor_id
,dbo.item.uom
,dbo.item.upc_num
,dbo.item.item_desc
,RIGHT(dbo.auditlog.pallet_id, 8) AS pallet_id
,dbo.auditlog.abs_num
,dbo.auditlog.item_qty
,dbo.auditlog.lot
,dbo.auditlog.packer
,auditlog.comments
,auditlog.date_time
,rank() OVER (
PARTITION BY auditlog.comments ORDER BY auditlog.date_time desc
) AS CorrectTrans
FROM dbo.auditlog
INNER JOIN dbo.pomst ON dbo.auditlog.co_num = dbo.pomst.co_num
AND dbo.auditlog.wh_num = dbo.pomst.wh_num
AND dbo.auditlog.po_number = dbo.pomst.po_number
AND dbo.auditlog.po_suffix = dbo.pomst.po_suffix
INNER JOIN dbo.item ON dbo.auditlog.co_num = dbo.item.co_num
AND dbo.auditlog.wh_num = dbo.item.wh_num
AND dbo.auditlog.abs_num = dbo.item.abs_num
WHERE (dbo.pomst.co_num = 'AC01')
AND (dbo.pomst.wh_num = 'KU22')
AND (dbo.pomst.row_status = 'C')
AND (dbo.auditlog.trans_type = 're')
AND item_qty NOT LIKE '-%'
)
Select * from cte
where CorrectTrans = 1
Add select and group by and use your existing query as a sub query.
Try ..
select max([CorrectTrans]), Vendor_Id, Item_qty, Lot, Pallet_id
from (
-- Your existing query --
SELECT DISTINCT dbo.pomst.co_num
,dbo.pomst.wh_num
,dbo.pomst.po_number
,dbo.pomst.po_suffix
,dbo.pomst.vendor_id
,dbo.item.uom
,dbo.item.upc_num
,dbo.item.item_desc
,RIGHT(dbo.auditlog.pallet_id, 8) AS pallet_id
,dbo.auditlog.abs_num
,dbo.auditlog.item_qty
,dbo.auditlog.lot
,dbo.auditlog.packer
,auditlog.comments
,auditlog.date_time
,rank() OVER (
PARTITION BY auditlog.comments ORDER BY auditlog.date_time ASC
) AS CorrectTrans
FROM dbo.auditlog
INNER JOIN dbo.pomst ON dbo.auditlog.co_num = dbo.pomst.co_num
AND dbo.auditlog.wh_num = dbo.pomst.wh_num
AND dbo.auditlog.po_number = dbo.pomst.po_number
AND dbo.auditlog.po_suffix = dbo.pomst.po_suffix
INNER JOIN dbo.item ON dbo.auditlog.co_num = dbo.item.co_num
AND dbo.auditlog.wh_num = dbo.item.wh_num
AND dbo.auditlog.abs_num = dbo.item.abs_num
WHERE (dbo.pomst.co_num = 'AC01')
AND (dbo.pomst.wh_num = 'KU22')
AND (dbo.pomst.row_status = 'C')
AND (dbo.auditlog.trans_type = 're')
AND item_qty NOT LIKE '-%'
-- =======================================
) x
group by Vendor_id, Item_qty, Lot, Pallet_id

How to use parameters in a SQL query with NOT EXISTS?

How can I change following query, so that I'm able to parameterize the SparePartNames?
It returns all ID's of repairs where not all mandatory spareparts were changed, in other words where at least one part is missing.
Note that the number of spareparts might change in future not only the names. Is it possible without using a stored procedure with dynamic SQL? If not, how could this SP look like?
Edit: Note that i do not need to know how to pass a list/array as parameter, this is asked myriads of time on SO. I've also already a Split table-valued-function. I'm just wondering how i could rewrite the query to be able to join(or whatever) with a list of mandatory parts, so that i'll find all records where at least one part is missing. So is it possible to use a varchar-parameter like '1264-3212,1254-2975' instead of a list of NOT EXISTS? Sorry for the confusion if it was not clear in the first place.
SELECT d.idData
FROM tabData d
INNER JOIN modModel AS m ON d.fiModel = m.idModel
WHERE (m.ModelName = 'MT27I')
AND (d.fiMaxServiceLevel >= 2)
AND (d.Manufacture_Date < '20120511')
AND (NOT EXISTS
(SELECT NULL
FROM tabDataDetail AS td
INNER JOIN tabSparePart AS sp ON sp.idSparePart = td.fiSparePart
WHERE (td.fiData = d.idData)
AND (sp.SparePartName = '1264-3212'))
OR (NOT EXISTS
(SELECT NULL
FROM tabDataDetail AS td
INNER JOIN tabSparePart AS sp ON sp.idSparePart = td.fiSparePart
WHERE (td.fiData = d.idData)
AND (sp.SparePartName = '1254-2975'))
)
)
Unfortunately I don't see how I could use sp.SparePartName IN/NOT IN(#sparePartNames) here.
One way to do it is to create a function to split delimited strings:
CREATE FUNCTION [dbo].[Split]
(
#Delimiter char(1),
#StringToSplit varchar(512)
)
RETURNS table
AS
RETURN
(
WITH Pieces(pieceNumber, startIndex, delimiterIndex)
AS
(
SELECT 1, 1, CHARINDEX(#Delimiter, #StringToSplit)
UNION ALL
SELECT pieceNumber + 1, delimiterIndex + 1, CHARINDEX(#Delimiter, #StringToSplit, delimiterIndex + 1)
FROM Pieces
WHERE delimiterIndex > 0
)
SELECT
SUBSTRING(#StringToSplit, startIndex, CASE WHEN delimiterIndex > 0 THEN delimiterIndex - startIndex ELSE 512 END) AS Value
FROM Pieces
)
populate a table variable with the spare part names:
DECLARE #SpareParts TABLE
(
SparePartName varchar(50) PRIMARY KEY CLUSTERED
);
INSERT INTO #SpareParts
SELECT Value FROM dbo.Split(',', '1264-3212,1254-2975');
and then join to the table variable:
SELECT d.idData
FROM tabData d
INNER JOIN modModel AS m ON d.fiModel = m.idModel
WHERE (m.ModelName = 'MT27I')
AND (d.fiMaxServiceLevel >= 2)
AND (d.Manufacture_Date < '20120511')
AND EXISTS (
SELECT 1
FROM tabDataDetail AS td
INNER JOIN tabSparePart AS sp ON sp.idSparePart = td.fiSparePart
LEFT JOIN #SpareParts AS s ON s.SparePartName = sp.SparePartName
WHERE td.fiData = d.idData
AND s.SparePartName IS NULL
)
Assuming there is (or will be) a table or view of mandatory spare parts, a list of exists can be replaced with a left join to tabDataDetail / tabSparePart pair on SparePartName; non-matches are reported back using td.fiSparePart is null.
; with mandatorySpareParts (SparePartName) as (
select '1264-3212'
union all
select '1254-2975'
)
SELECT d.idData
FROM tabData d
INNER JOIN modModel AS m ON d.fiModel = m.idModel
WHERE (m.ModelName = 'MT27I')
AND (d.fiMaxServiceLevel >= 2)
AND (d.Manufacture_Date < '20120511')
AND exists
(
SELECT null
from mandatorySpareParts msp
left join ( tabDataDetail AS td
INNER JOIN tabSparePart AS sp
ON sp.idSparePart = td.fiSparePart
AND td.fiData = d.idData
)
ON msp.SparePartName = sp.SparePartName
WHERE td.fiSparePart is null
)
Part names should be replaced by their id's, which would simplify left join and speed the query up.
EDIT: i've errorneously left filtering of td in where clause, which invalidated left join. It is now in ON clause where it belongs.
Use a table-variable and join on that.