Converting a script from MSSQL to PL/pgSQL - sql

I just started working with the EVE static dump, which is just a lot of tables with data about the game, such as a list of what solar systems connect, which is what I'm dealing with.
I want to make a webpage that lets you filter out systems, and the first step is getting a list of systems nearby, with the distance to them.
I found a script that does it for MSSQL
--By Joanna Davaham http://forum.eveuniversity.org/viewtopic.php?t=44601&p=396107#p424943
--set values
DECLARE #jumpsAway INT =10
DECLARE #MiddleSystemName VARCHAR(50) = 'Aldrat'
DECLARE #Level INT =1
IF OBJECT_ID('tempdb..#map') IS NOT NULL
DROP TABLE #map
CREATE TABLE #map
(fromSolarSystemID INT, toSolarSystemID INT, Level INT)
INSERT INTO #map
SELECT -1, mSS.solarSystemID, 0 FROM mapSolarSystems mSS
WHERE mSS.solarSystemName= #MiddleSystemName
WHILE #Level <= #jumpsAway
BEGIN
INSERT INTO #map
SELECT mSSJ.fromSolarSystemID, mSSJ.toSolarSystemID, #Level FROM mapSolarSystemJumps mSSJ
WHERE mSSJ.fromSolarSystemID IN (SELECT toSolarSystemID FROM #map WHERE Level = #Level-1)
AND mSSJ.fromSolarSystemID NOT IN (SELECT fromSolarSystemID FROM #map)
SET #Level=#Level+1
END
SELECT m.*, mSS.solarSystemName, mSS.security FROM #map m
JOIN mapSolarSystems mSS ON m.toSolarSystemID=mSS.solarSystemID
--WHERE mSS.security<0.45 --uncomment to check all nearby lowsec system
I know that I could probably just use the MSSQL version of the dump, but I also want to be learning more about how to use PostgreSQL better.
I understand what it's doing and everything, but I just don't understand PL/pgSQL well enough to make it work.
My attempt is
CREATE FUNCTION near(VARCHAR, INTEGER) RETURNS TABLE(fromID INT,toID INT,jumps INT,name VARCHAR,security VARCHAR) AS $$
DECLARE --Declaration from here http://www.postgresql.org/docs/9.1/static/plpgsql-declarations.html
MiddleSystemName ALIAS FOR $1;
jumpsAway ALIAS FOR $2;
jumps INTEGER :=1;
BEGIN
--http://stackoverflow.com/questions/11979154/select-into-to-create-a-table-in-pl-pgsql
CREATE TEMP TABLE map AS
SELECT -1, mSS.solarSystemID, 0
FROM mapSolarSystems mSS
WHERE mSS.solarSystemName= MiddleSystemName;
LOOP
--http://www.postgresql.org/docs/9.1/static/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN
--If you don't do it with execute, you can only do one row, I guess?
EXECUTE 'SELECT
|| mSSJ.fromSolarSystemID,
|| mSSJ.toSolarSystemID,
|| $1
|| FROM
|| mapSolarSystemJumps mSSJ
|| WHERE
|| mSSJ.fromSolarSystemID EXISTS (SELECT toSolarSystemID FROM map WHERE jumps = $1 - 1)
|| AND mSSJ.fromSolarSystemID NOT EXISTS (SELECT fromSolarSystemID FROM map)'
INTO map
USING jumps;
jumps := jumps + 1
EXIT WHEN jumps > jumpsAway;
END LOOP;
RETURN QUERY SELECT m.*,mSS.solarSystemName, mSS.security FROM JOIN mapSolarSystems mSS ON m.toSolarSystemID = mSS.solarSystemID;
END;
$$ LANGUAGE plpgsql;
And the error that produces is
Error is
ERROR: "map" is not a known variable
LINE 27: INTO map
^
Thanks for all the help.

PL/pgSQL
This should be a valid translation to plpgsql:
CREATE OR REPLACE FUNCTION f_near(_middlesystemname text, _jumpsaway int)
RETURNS TABLE(fromid int, toid int, jumps int, name text, security text) AS
$func$
DECLARE
_jumps integer;
BEGIN
CREATE TEMP TABLE map AS
SELECT -1 AS "fromSolarSystemID"
,m."solarSystemID" AS "toSolarSystemID"
,0 AS level
FROM "mapSolarSystems" m
WHERE "solarSystemName" = _middlesystemname;
-- potentially add indexes on the temp table and ANALYZE if it gets big
FOR _jumps IN 1 .. _jumpsaway LOOP
INSERT INTO map ("fromSolarSystemID", "toSolarSystemID", level)
SELECT sj."fromSolarSystemID", sj."toSolarSystemID", _jumps AS level
FROM "mapSolarSystemJumps" sj
JOIN map m ON m."toSolarSystemID" = sj."fromSolarSystemID"
AND m."level" = _jumps - 1
LEFT JOIN map mx ON mx."fromSolarSystemID" = sj."fromSolarSystemID"
WHERE mx."fromSolarSystemID" IS NULL;
END LOOP;
RETURN QUERY
SELECT m.*, s."solarSystemName", s."security"
FROM map m
JOIN "mapSolarSystems" s ON m."toSolarSystemID" = s."solarSystemID";
END
$func$ LANGUAGE plpgsql;
RECURSIVE CTE - doesn't seem to work
This short SQL query with a recursive CTE should have done it:
WITH RECURSIVE map AS (
SELECT -1 AS fromsolarsystemid, m.solarsystemid, 0 AS level
FROM mapsolarsystems m
WHERE m.solarsystemname = from_id
UNION ALL
SELECT sj.fromsolarsystemid, sj.tosolarsystemid, level + 1
FROM mapsolarsystemjumps sj
JOIN map m USING (level)
LEFT JOIN map mx USING (fromsolarsystemid)
WHERE sj.fromsolarsystemid = m.tosolarsystemid
AND mx.fromsolarsystemid IS NULL
AND m.level < 10 -- jumpsAway
)
SELECT m.*, s.solarsystemname, s.security
FROM map m
JOIN mapsolarsystems s ON m.tosolarsystemid = s.solarsystemid
-- WHERE s.security < 0.45 -- uncomment to check all nearby lowsec system
However:
ERROR: recursive reference to query "map" must not appear within an outer join
LINE 9: LEFT JOIN map mx USING (fromsolarsystemid)

Related

Trigger that prevents update of column based on result of the user defined function

We have DVD Rental company. In this particular scenario we consider only Member, Rental and Membership tables.
The task is to write a trigger that prevents a customer from being shipped a DVD
if they have reached their monthly limit for DVD rentals as per their membership contract using the function.
My trigger leads to infinite loop. It works without While loop, but then it does not work properly, if I consider multiple updates to the Rental table. Where I am wrong?
-- do not run, infinite loop
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE #MemberId INT
DECLARE #RentalId INT
SELECT * INTO #TempTable FROM inserted
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
IF UPDATE(RentalShippedDate)
BEGIN
IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
END;
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
END;
My function looks as follows:
CREATE OR ALTER FUNCTION dvd_numb_left(#member_id INT)
RETURNS #tab_dvd_numb_left TABLE(MemberId INT, Name VARCHAR(50), TotalDvdLeft INT, AtTimeDvdLeft INT)
AS
BEGIN
DECLARE #name VARCHAR(50)
DECLARE #dvd_total_left INT
DECLARE #dvd_at_time_left INT
DECLARE #dvd_limit INT
DECLARE #dvd_rented INT
DECLARE #dvd_at_time INT
DECLARE #dvd_on_rent INT
SET #dvd_limit = (SELECT Membership.MembershipLimitPerMonth FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = #member_id))
SET #dvd_rented = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE CONCAT(month(Rental.RentalShippedDate), '.', year(Rental.RentalShippedDate)) = CONCAT(month(GETDATE()), '.', year(GETDATE())) AND Rental.MemberId = #member_id)
SET #dvd_at_time = (SELECT Membership.DVDAtTime FROM Membership
WHERE Membership.MembershipId = (SELECT Member.MembershipId FROM Member WHERE Member.MemberId = #member_id))
SET #dvd_on_rent = (SELECT COUNT(Rental.MemberId) FROM Rental
WHERE Rental.MemberId = #member_id AND Rental.RentalReturnedDate IS NULL)
SET #name = (SELECT CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) FROM Member WHERE Member.MemberId = #member_id)
SET #dvd_total_left = #dvd_limit - #dvd_rented
SET #dvd_at_time_left = #dvd_at_time - #dvd_on_rent
IF #dvd_total_left < 0
BEGIN
SET #dvd_total_left = 0
SET #dvd_at_time_left = 0
INSERT INTO #tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(#member_id, #name, #dvd_total_left, #dvd_at_time_left)
RETURN;
END
INSERT INTO #tab_dvd_numb_left(MemberId, Name, TotalDvdLeft, AtTimeDvdLeft)
VALUES(#member_id, #name, #dvd_total_left, #dvd_at_time_left)
RETURN;
END;
Will be glad for any advice.
Your main issue is that even though you populate #TempTable you never pull any values from it.
CREATE OR ALTER TRIGGER trg_Rental_StopDvdShip
ON RENTAL
FOR UPDATE
AS
BEGIN
DECLARE #MemberId INT, #RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 #RentalID = RentalId, #MemberId = MemberId FROM #TempTable;
-- Select our values to its working
-- SELECT #RentalID, #MemberId;
IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
BEGIN
ROLLBACK
RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
END;
An easy way to debug simply triggers like this is to copy the T-SQL out and then create an #Inserted table variable e.g.
DECLARE #Inserted table (RentalId INT, MemberId INT);
INSERT INTO #Inserted (RentalId, MemberId)
VALUES (1, 1), (2, 2);
DECLARE #MemberId INT, #RentalId INT;
-- Move test for column update to the first test as it applies to the entire update, not per row.
-- IF UPDATE(RentalShippedDate)
BEGIN
SELECT * INTO #TempTable FROM #inserted;
WHILE (EXISTS (SELECT RentalId FROM #TempTable))
BEGIN
-- Actually pull some information from #TempTable - this wasn't happening before
SELECT TOP 1 #RentalID = RentalId, #MemberId = MemberId FROM #TempTable;
-- Select our values to its working
SELECT #RentalID, #MemberId;
-- IF (SELECT TotalDvdLeft FROM dvd_numb_left(#MemberId)) <= 0
-- BEGIN
-- ROLLBACK
-- RAISERROR ('YOU HAVE REACHED MONTHLY LIMIT FOR DVD RENTALS', 16, 1)
-- END;
-- Delete the current handled row
DELETE FROM #TempTable WHERE RentalID = #RentalId
END;
-- For neatness I always drop temp tables, makes testing easier also
DROP TABLE #TempTable;
END;
Note: throw is the recommended way to throw an error instead of raiserror.
Another thing to consider is that you must try to transform your UDF into an inline TVF because of some side effects.
Like this one:
CREATE OR ALTER FUNCTION dvd_numb_left(#member_id INT)
RETURNS TABLE
AS
RETURN
(
WITH
TM AS
(SELECT Membership.MembershipLimitPerMonth AS dvd_limit,
Membership.DVDAtTime AS dvd_at_time,
CONCAT(Member.MemberFirstName, ' ', Member.MemberLastName) AS [name]
FROM Membership AS MS
JOIN Member AS M
ON MS.MembershipId = M.MembershipId
WHERE M.MemberId = #member_id
),
TR AS
(SELECT COUNT(Rental.MemberId) AS dvd_rented
FROM Rental
WHERE YEAR(Rental.RentalShippedDate ) = YEAR(GETDATE)
AND MONTH(Rental.RentalShippedDate ) = MONTH(GETDATE)
AND Rental.MemberId = #member_id
)
SELECT MemberId, [Name],
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_limit - dvd_rented END AS TotalDvdLeft,
CASE WHEN dvd_limit - dvd_rented < 0 THEN 0 ELSE dvd_at_time - dvd_on_rent END AS AtTimeDvdLeft
FROM TM CROSS JOIN TR
);
GO
Which will be much more efficient.
The absolute rule to have performances is: TRY TO STAY IN A "SET BASED" CODE instead of iterative code.
The above function can be optimized by the optimzer whilet yours cannot and will needs 4 access to the same tables.

Compare two columns and make the insert

I want to compare two columns which come from two different tables.
One of the columns, I need to make SUM for all rows with identity let's say 3 and store to a variable.
After that, compare with one row from other table for same identity 3 and to INSERT something ELSE to BREAK if first_column <= second_column.
Can someone suggest some query for this? For Postgresql...
CREATE OR REPLACE FUNCTION "SA_PRJ".usp_add_timesheet_test(p_uid integer, p_project_id integer, p_allocated_time numeric, p_achieved_time numeric, p_task_desc character varying, p_obs character varying, p_date timestamp without time zone)
RETURNS character varying AS
$BODY$
DECLARE sum_alloc_time numeric;
DECLARE alloc_hours integer;
DECLARE fld_id integer;
DECLARE alloc_id integer;
BEGIN
if not "SA_ADM".usp_check_permission(p_uid, 'SA_PRJ', 'usp_add_timesheet_record') then
raise exception 'User ID % dont have permission!', p_uid;
end if;
select a.fld_id into alloc_id from "SD_PRJ".tbl_project_allocation a where a.fld_emp_id = p_uid and a.fld_project_id = p_project_id;
SELECT SUM(fld_allocated_time)
INTO sum_alloc_time
FROM "SD_PRJ".tbl_project_timesheet
WHERE fld_project_id = p_project_id;
SELECT p.fld_allocated_days, p.fld_id
INTO alloc_hours, fld_id
FROM "SD_PRJ".tbl_project p
JOIN "SD_PRJ".tbl_project_timesheet t USING (fld_id)
WHERE t.fld_project_id = p_project_id;
IF #sum_alloc_time <= #alloc_hours THEN
INSERT INTO "SD_PRJ".tbl_project_timesheet
(fld_emp_id, fld_project_id, fld_is_allocated, fld_allocated_time
, fld_achieved_time, fld_task_desc, fld_obs, fld_date)
VALUES (p_uid, p_project_id, coalesce(alloc_id,0), p_allocated_time
, p_achieved_time, p_task_desc, p_obs, p_date);
RAISE NOTICE 'INSERT OK!';
ELSE
RAISE NOTICE 'NOT OK';
END IF;
END
1.tbl_project (fld_id, fld_allocated_days,fld_project_id)
2.tbl_project_timesheet(fld_id,fld_allocated_time,fld_project_id), all INTEGER
I have this , but dosen't work as I wish.Thanks
I think one problem is here:
SELECT p.fld_allocated_days, p.fld_id
INTO alloc_hours, fld_id
FROM "SD_PRJ".tbl_project p
JOIN "SD_PRJ".tbl_project_timesheet t USING (fld_id)
WHERE t.fld_project_id = p_project_id;
That will cough (I think) whenever the select query returns more than one row i.e. whenever tbl_project_timesheet has more than one record for a fld_id,project_id combination.
Anyway. Here's a partial, simplified answer, but hopefully you get the idea...
I wouldn't use local variables. Do the insert in one step:
INSERT INTO timesheet(emp_id,project_id) -- other columns
SELECT
p_uid,p.fld_project_id -- other columns
FROM
projects p
INNER JOIN
(SELECT SUM(fld_allocated_time) as sumtime
FROM timesheet t WHERE fld_project_id = p_project_id) as sumtime_subquery
ON p.fld_allocated_days < sumtime -- just join on the allocated time
WHERE p.fld_project_id = p_project_id;
Now, you need to know if anything was actually inserted. I think you can use the RETURNING option of the INSERT statement, e.g. from here (caveat - I have never used RETURNING, nor set a local variable from a with statement):
WITH ROWS AS (
INSERT INTO timesheet(emp_id,project_id) -- other columns
SELECT
p_uid,p.fld_project_id -- other columns
FROM
projects p
INNER JOIN
(SELECT SUM(fld_allocated_time) as sumtime
FROM timesheet t WHERE fld_project_id = p_project_id) as sumtime_subquery
ON p.fld_allocated_days < sumtime -- just join on the allocated time
WHERE p.fld_project_id = p_project_id
RETURNING 1
)
SELECT COUNT(*) into l_updatedCount FROM rows; -- you have to declare l_updatedCount
-- Now an if statement to handle l_updatedCount

Using a Stored Procedure in a View with Postgresql

I have been trying to create a View where one of the column pending_amount gets its value as a result of a stored procedure execution.
The stored procedure is pending_stock(int,int) and returns an integer. The view is created successfully but when i try to perform any query like select on the this view it takes ever to return a value.
CREATE OR REPLACE VIEW view_production_parts AS
SELECT p.part_id, p.min_amount, gp.part_num, gp.description
, p.quantity_available
, p.quantity_total - p.quantity_available AS quantity_alloc
, p.quantity_total
, (SELECT pending_stock(p.part_id, 0) AS pending_stock) AS pending_amount
, p.production_run
, CASE
WHEN ppur.purchased_part_id IS NOT NULL THEN true
ELSE false
END AS is_purchased_part, ppur.purchased_part_id, p.store_move_type_id
, gp.part_status_id, p.default_location
, COALESCE(pwoh.part_work_order_hold_id, 0) AS part_work_order_hold_id
FROM general_part gp
JOIN part p ON gp.part_id = p.part_id
LEFT JOIN purchased_part ppur ON ppur.part_id = p.part_id
LEFT JOIN part_work_order_hold pwoh ON pwoh.part_id = p.part_id
ORDER BY gp.part_num;
Can a stored procedure be used in a view? If used, Is my declaration correct?
Find the result from of this query at explain.depesz.com:
EXPLAIN ANALYZE SELECT count(*) FROM view_production_parts
I am using Postgres 8.4.
Function definition for pending_stock(int,int):
CREATE OR REPLACE FUNCTION pending_stock(var_part_id integer
, var_pattern_id integer)
RETURNS integer AS
$BODY$
declare
r record;
var_qty_expected integer;
var_qty_moved_to_stock integer;
var_total_stock_moved_out integer;
var_actual_qty integer;
begin
var_total_stock_moved_out := 0;
var_qty_expected := 0;
for r in
select work_order_id,quantity_expected
from view_work_orders
where part_id = var_part_id and open = 'TRUE'
and quantity_allocated is null and quantity_expected >= quantity_actual
loop
var_qty_expected = var_qty_expected + r.quantity_expected;
select sum(quantity) from view_work_order_move_parts_details
where source_work_order_id = r.work_order_id
and part_id = var_part_id into var_qty_moved_to_stock;
if var_qty_moved_to_stock is null then
var_qty_moved_to_stock = 0;
end if;
var_total_stock_moved_out = var_total_stock_moved_out
+ var_qty_moved_to_stock;
end loop;
var_actual_qty := var_qty_expected - var_total_stock_moved_out;
if var_actual_qty > 0 then
return var_actual_qty;
else
return 0;
end if;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE STRICT
COST 100;
ALTER FUNCTION pending_stock(integer, integer) OWNER TO postgres;
View
You don't need a subquery for the function call. And you can simplify some other minor details:
CREATE OR REPLACE VIEW view_production_parts AS
SELECT p.part_id, p.min_amount
, gp.part_num, gp.description, p.quantity_available
, p.quantity_total - p.quantity_available AS quantity_alloc
, p.quantity_total
, pending_stock(gp.part_id, 0) AS pending_amount
, p.production_run
,(ppur.purchased_part_id IS NOT NULL) AS is_purchased_part
, ppur.purchased_part_id, p.store_move_type_id, gp.part_status_id
, p.default_location
, COALESCE(pwoh.part_work_order_hold_id, 0) AS part_work_order_hold_id
FROM general_part gp
JOIN part p USING (part_id)
LEFT JOIN purchased_part ppur USING (part_id)
LEFT JOIN part_work_order_hold pwoh USING (part_id)
ORDER BY gp.part_num;
Other than that the VIEW definition looks fine.
Function
Can be largely simplified:
CREATE OR REPLACE FUNCTION pending_stock(var_part_id integer
, var_pattern_id integer)
RETURNS integer AS
$func$
DECLARE
r record;
var_qty_expected integer := 0;
var_total_stock_moved_out integer := 0;
BEGIN
FOR r IN
SELECT work_order_id, quantity_expected
FROM view_work_orders
WHERE part_id = var_part_id
AND open = 'TRUE' -- A string instead of a boolean?
AND quantity_allocated IS NULL
AND quantity_expected >= quantity_actual
LOOP
var_qty_expected := var_qty_expected + r.quantity_expected;
SELECT var_total_stock_moved_out + COALESCE(sum(quantity), 0)
FROM view_work_order_move_parts_details
WHERE source_work_order_id = r.work_order_id
AND part_id = var_part_id
INTO var_total_stock_moved_out;
END LOOP;
RETURN GREATEST(var_qty_expected - var_total_stock_moved_out, 0);
END
$func$ LANGUAGE plpgsql
Major points
Generally, assignments are comparatively expensive in plpgsql. Every assignment is executed with a (very simple and fast) SELECT statement internally. Try to use fewer of them.
You can init variables at declaration time. No need for another statement.
The assignment operator in plpgsql is :=. = works, but is undocumented.
Use COALESCE() to catch NULL values.
The function parameter var_pattern_id is never used. This is probably not the full function definition.
The whole final part can be replaced with a single statement using GREATEST
Superior query
Now, this cleaned up function will be a bit faster, but not much. Your whole design of looping repeatedly is extremely inefficient. It results in correlated subqueries that loop through correlated subqueries yet again. Performance nightmare.
Recast the problem as set-based operation to make it faster. Well, a lot faster.
SELECT e.part_id
,GREATEST(COALESCE(sum(e.quantity_expected), 0)
- COALESCE(sum(m.total_stock_moved_out), 0), 0)
FROM view_work_orders e
LEFT JOIN (
SELECT source_work_order_id AS work_order_id
,COALESCE(sum(quantity), 0) AS total_stock_moved_out
FROM view_work_order_move_parts_details
WHERE part_id = var_part_id
GROUP BY 1
) m USING (work_order_id)
WHERE e.part_id = var_part_id
AND e.open = 'TRUE'
AND e.quantity_allocated IS NULL
AND e.quantity_expected >= e.quantity_actual
GROUP BY 1;
Superior view
Integrate this into the original query / view:
CREATE OR REPLACE VIEW view_production_parts AS
SELECT p.part_id, p.min_amount
,gp.part_num, gp.description, p.quantity_available
,p.quantity_total - p.quantity_available AS quantity_alloc
,p.quantity_total
,x.pending_amount
,p.production_run
,(ppur.purchased_part_id IS NOT NULL) AS is_purchased_part
,ppur.purchased_part_id, p.store_move_type_id, gp.part_status_id
,p.default_location
,COALESCE(pwoh.part_work_order_hold_id, 0) AS part_work_order_hold_id
FROM general_part gp
JOIN part p USING (part_id)
LEFT JOIN purchased_part ppur USING (part_id)
LEFT JOIN part_work_order_hold pwoh USING (part_id)
LEFT JOIN (
SELECT e.part_id
,GREATEST(COALESCE(sum(e.quantity_expected), 0)
- COALESCE(sum(m.total_stock_moved_out), 0)
, 0) AS pending_amount
FROM view_work_orders e
LEFT JOIN (
SELECT source_work_order_id AS work_order_id
,sum(quantity) AS total_stock_moved_out
FROM view_work_order_move_parts_details
WHERE part_id = var_part_id
GROUP BY 1
) m USING (work_order_id)
WHERE e.part_id = var_part_id
AND e.open = 'TRUE'
AND e.quantity_allocated IS NULL
AND e.quantity_expected >= e.quantity_actual
GROUP BY 1
) x USING (part_id)
ORDER BY gp.part_num;
Untested, obviously.

how to make an outer join with the calling of other procedure

I have a stored procedure :
procedure qr_get_dep_boss(...,...)
takes two params(year, main_code) and returning one record the boss_num and his name .
I want to create another procedure and make a loop on the previous procedure calling , making an outer join with the result of another query
SELECT
year,
main_code,
name,
CASE
WHEN father_code in(-1) THEN NULL
ELSE father_code
END AS father_code,main_code || '_' || year AS main_id,
(SELECT COUNT(*)
FROM sm_r_build
WHERE father_code = sc.main_code
AND year = (SELECT MAX(year)
FROM st_quit_info)) childcount,
main_code||'_'|| father_code AS serial
FROM sm_r_build sc
WHERE year=(SELECT MAX(year)FROM st_quit_info)
So I want the result:
year,main_code,name,father_code,main_id,childcount,serial,boss_num,boss_name
I try this :
create procedure new_get_alldepwithboss()
returning int as year , int as main_code ,nvarchar(100) as name,int as father_code,nvarchar(255) as main_id,int as childcount,int as ll_boss_num,nvarchar(100) as ll_boss_name
define ll_year int;
define ll_main_code int;
define ll_name nvarchar(100);
define ll_father_code int;
define ll_main_id nvarchar(255) ;
define ll_childcount int;
define ll_boss_num int;
define ll_boss_name nvarchar(100);
foreach
SELECT year,main_code,name,CASE WHEN father_code in(-1) THEN NULL ELSE father_code END AS father_code,main_code || '_' || year AS main_id, (SELECT COUNT(*)
FROM sm_r_build
WHERE father_code=sc.main_code AND year= (SELECT MAX(year)
FROM st_quit_info)) childcount ,a.emp_num,a.emp_name
INTO ll_year ,ll_main_code,ll_name ,ll_father_code ,ll_main_id ,ll_childcount ,ll_boss_num,ll_boss_name
FROM sm_r_build sc , TABLE(FUNCTION qr_get_dep_boss(ll_main_code, ll_year))AS a(emp_num,emp_name)
WHERE year=(SELECT MAX(year)FROM st_quit_info)
return ll_year , ll_main_code, ll_name,ll_father_code,ll_main_id, ll_childcount , ll_boss_num,ll_boss_name with resume;
end foreach
end procedure
but in vain !
In case of SQL server I would recommend using rather function than store procedure. Header of function should look:
CREATE FUNCTION qr_get_dep_boss( #year DATE, #main_code INT)
RETURN #t TABLE ( boss_num INT, boss_name VARCHAR(50))
AS
... ( here boss_num and boss_name should be set )
In your main query you can use this function by:
SELECT year, main_code, ... , t.bos_num , t.boss_name
FROM ...
CROSS APPLY
qr_get_dep_boss(year, main_code) t
But I am not sure if it is possible in Informix, in case not you can always define two functions that returns single value. GL!
If I understand right, you don't need the second procedure.
What you need to do is use the RETURN xxx WITH RESUME into the procedure qr_get_dep_boss()
A example copied from the manual .
CREATE FUNCTION series (limit INT, backwards INT) RETURNING INT;
DEFINE i INT;
FOR i IN (1 TO limit)
RETURN i WITH RESUME;
END FOR;
IF backwards = 0 THEN
RETURN;
END IF;
FOR i IN (limit TO 1 STEP -1)
RETURN i WITH RESUME;
END FOR;
END FUNCTION; -- series

How to find all IDs of children recursively?

I would like to get all IDs from children in a tree with MySQL only.
I have a table like this:
ID parent_id name
1 0 cat1
2 1 subcat1
3 2 sub-subcat1
4 2 sub-subcat2
5 0 cat2
Now I'm trying to get all child IDs for cat1 (2,3,4) recursively. Is there any way how to achieve that?
There are two basic methods for doing this: adjacency lists and nested lists. Take a look at Managing Hierarchical Data in MySQL.
What you have is an adjacency list. No there isn't a way of recursively grabbing all descendants with a single SQL statement. If possible, just grab them all and map them all in code.
Nested sets can do what you want but I tend to avoid it because the cost of inserting a record is high and it's error-prone.
Here is a simple single-query MySql-solution:
SELECT GROUP_CONCAT(Level SEPARATOR ',') FROM (
SELECT #Ids := (
SELECT GROUP_CONCAT(`ID` SEPARATOR ',')
FROM `table_name`
WHERE FIND_IN_SET(`parent_id`, #Ids)
) Level
FROM `table_name`
JOIN (SELECT #Ids := <id>) temp1
) temp2
Just substitute <id> with the parent element's ID.
This will return a string with the IDs of all descendants of the element with ID = <id>, separated by ,. If you would rather have multiple rows returned, with one descendant on each row, you can use something like this:
SELECT *
FROM `table_name`
WHERE FIND_IN_SET(`ID`, (
SELECT GROUP_CONCAT(Level SEPARATOR ',') FROM (
SELECT #Ids := (
SELECT GROUP_CONCAT(`ID` SEPARATOR ',')
FROM `table_name`
WHERE FIND_IN_SET(`parent_id`, #Ids)
) Level
FROM `table_name`
JOIN (SELECT #Ids := <id>) temp1
) temp2
))
Including the root/parent element
The OP asked for the children of an element, which is answered above. In some cases it might be useful to include the root/parent element in the result. Here are my suggested solutions:
Comma-separated string of ids:
SELECT GROUP_CONCAT(Level SEPARATOR ',') FROM (
SELECT <id> Level
UNION
SELECT #Ids := (
SELECT GROUP_CONCAT(`ID` SEPARATOR ',')
FROM `table_name`
WHERE FIND_IN_SET(`parent_id`, #Ids)
) Level
FROM `table_name`
JOIN (SELECT #Ids := <id>) temp1
) temp2
Multiple rows:
SELECT *
FROM `table_name`
WHERE `ID` = <id> OR FIND_IN_SET(`ID`, (
SELECT GROUP_CONCAT(Level SEPARATOR ',') FROM (
SELECT #Ids := (
SELECT GROUP_CONCAT(`ID` SEPARATOR ',')
FROM `table_name`
WHERE FIND_IN_SET(`parent_id`, #Ids)
) Level
FROM `table_name`
JOIN (SELECT #Ids := <id>) temp1
) temp2
))
You could probably do it with a stored procedure, if that's an option for you.
Otherwise you can't do it with a single sql-statement.
Ideally you should make the recursive calls to walk the tree from your program
create table it should be look like below
DROP TABLE IF EXISTS `parent_child`;
CREATE TABLE `parent_child` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`parent_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;
insert into `parent_child`(`id`,`name`,`parent_id`)
values (1,'cat1',0),(2,'subcat1',1),
(3,'sub-subcat1',2),(4,'sub-subcat2',2),
(5,'cat2',0);
Create function for getting parent child element
DELIMITER $$
USE `yourdatabase`$$
DROP FUNCTION IF EXISTS `GetAllNode1`$$
CREATE DEFINER=`root`#`localhost` FUNCTION `GetAllNode1`(GivenID INT) RETURNS TEXT CHARSET latin1
DETERMINISTIC
BEGIN
DECLARE rv,q,queue,queue_children TEXT;
DECLARE queue_length,front_id,pos INT;
SET rv = '';
SET queue = GivenID;
SET queue_length = 1;
WHILE queue_length > 0 DO
SET front_id = queue;
IF queue_length = 1 THEN
SET queue = '';
ELSE
SET pos = LOCATE(',',queue) + 1;
SET q = SUBSTR(queue,pos);
SET queue = q;
END IF;
SET queue_length = queue_length - 1;
SELECT IFNULL(qc,'') INTO queue_children
FROM (SELECT GROUP_CONCAT(id) AS qc
FROM `parent_child` WHERE `parent_id` = front_id) A ;
IF LENGTH(queue_children) = 0 THEN
IF LENGTH(queue) = 0 THEN
SET queue_length = 0;
END IF;
ELSE
IF LENGTH(rv) = 0 THEN
SET rv = queue_children;
ELSE
SET rv = CONCAT(rv,',',queue_children);
END IF;
IF LENGTH(queue) = 0 THEN
SET queue = queue_children;
ELSE
SET queue = CONCAT(queue,',',queue_children);
END IF;
SET queue_length = LENGTH(queue) - LENGTH(REPLACE(queue,',','')) + 1;
END IF;
END WHILE;
RETURN rv;
END$$
DELIMITER ;
write query for desire output
SELECT GetAllNode1(id) FROM parent_child
or
SELECT GetAllNode1(id) FROM parent_child where id =1 //for specific parent's child element
Your question seems a bit imprecise. Why do you want to have them, and what do you mean by having them, "in a tree" ?
The table you've got IS (the relational way to represent) the tree.
If you want them "in a table" with rows that hold the pairs (ID 4 , ParentID 0), then you need your SQL engine's version of recursive SQL to do this, if that engine supports it.
I wouldn't know about MySQL specifically, but my understanding is that they once planned to implement recursive SQL using the same syntax as Oracle, i.e. with CONNECT BY.
If you look in your manual's table of contents for keywords such as "recursive queries" or "CONNECT BY", I imagine you should be able to find the answer.
(Sorry for not being able to provide a more ready-to-consume answer.)