T-SQL equivalent of Oracle's collection methods (first, last, next)? - sql

We're moving from Oracle to SQL Server and I'm converting a query from a table variable populated with a BULK COLLECT INTO query. I'm thinking of using a cursor (definitely open to other suggestions), but in the Oracle code that processes the query it's using Table_var.FIRST .NEXT and .LAST. Here's some sample code of how it's using these. It appears that the first/next/last are giving indexes into the table var's records.
TYPE Pers_DOB_LastInitial IS RECORD (
Person_ID Person.Person_ID%TYPE,
DOB Person.Birthdate%TYPE,
LastInitial VARCHAR2(1)
);
TYPE Dup_Table IS TABLE OF Pers_DOB_LastInitial INDEX BY BINARY_INTEGER;
Dup_Tab Dup_Table;
and a function that uses these types:
FUNCTION Last_In_Group( pStart NUMBER, pDOB Person.Birthdate%TYPE, pLastInitial VARCHAR2 )
RETURN NUMBER IS
vResult NUMBER;
BEGIN
IF pStart = Dup_Tab.LAST THEN
RETURN pStart;
END IF;
vResult := pStart;
FOR vIndex IN pStart .. Dup_Tab.LAST LOOP
IF Dup_Tab.EXISTS( vIndex ) THEN
IF Dup_Tab( vIndex ).DOB = pDOB AND Dup_Tab( vIndex ).LastInitial = pLastInitial THEN
vResult := vIndex;
ELSE
EXIT;
END IF;
END IF;
END LOOP;
RETURN vResult;
END Last_In_Group;
I don't need the coding done for me, just need to be pointed in the right direction. I'm thinking of using a cursor, but the only thing I'm familiar with is simply fetching the next record from a cursor in T-SQL and want to see if there are equivalent ways to reference row indexes for cursors (or temp tables).
EDIT: I've just discovered the following and am looking into it. Definitely open to hints on whether this is a good path to pursue or if cursors are still better.
http://www.sql-server-performance.com/2004/operations-no-cursors/2/
DECLARE #dupTab TABLE (
person_id numeric(8,0),
DOB date,
LastInitial char(1)
)
INSERT #dupTab
SELECT ...

Not 100% sure what you're asking, but have you looked at the new LEAD, LAG, FIRST_VALUE, and LAST_VALUE keywords in Sql Server 2012?

Assuming your table is called Dup_Tab, and #DOB and #LastInitial are variables you passed into your function, might this work?
select PersonID from(
select
rank = row_number() over (order by DOB DESC, LastInitial DESC),
PersonID
from dup_tab where DOB=#DOB and LastInitial=#LastInitial
) subgroup
where rank=1
It's just using the windowed Rank() function to assign each record a row number based on DOB/LastInitial ordered last-first and then you select the row with rank = 1, giving you the last record in the set of persons with a specific DOB and LastInitial.

I translated your sample code into it's SQL-Server counterpart. If you want to do row-by-row processing the best performing way is imo to use a FAST_FORWARD cursor.
code:
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[Last_In_Group]') AND xtype in (N'FN', N'IF', N'TF'))
drop function Last_In_Group
go
create function Last_In_Group(#pStart int, #pDOB date, #pLastInitial varchar(1))
returns int
AS
BEGIN
DECLARE #Dup_Tab TABLE(
Person_ID int,
DOB date,
LastInitial varchar(1)
)
-- populate the table with some data
insert #Dup_Tab values
(1, '31.12.2013', 'P'), (2, '24.12.2013', 'C'), (3, '24.12.2013', 'C')
declare #vResult int = 0,
#cDOB date,
#cLastInitial varchar(1)
if(#vResult = #pStart) return #pStart
set #vResult = #pStart
-- loop over your data with a fast cursor
declare cur cursor FAST_FORWARD for select DOB, LastInitial from #Dup_Tab
open cur
fetch next from cur
into #cDOB, #cLastInitial
while ##FETCH_STATUS = 0
begin
if(#cDOB = #pDOB and #cLastInitial = #pLastInitial)
set #vResult += 1
fetch next from cur
into #cDOB, #cLastInitial
end
return #vResult;
end
go
print dbo.Last_In_Group(1, '24.12.2013', 'C')
go
It might not work as your sample above but I hope this gives you some hint where to go further.

Related

How can I pass a list, array or string to be separated as a parameter to redshift

I'm trying to write a simple query with an in clause like so:
SELECT *
FROM storeupcsalesbyday
WHERE date >= '9/1/2020' AND date <= '9/10/2020' AND upc in ('0000000004011', '0000000094011')
I need to be able to pass the values in the in clause as a parameter, the number of values in the in clause are variable and could be one or thousands depending on the user input. In other sql databases I have solved this problem by creating a user defined function that takes a string, splits it on a delimiter and inserts the values in a temp table, then I would select all from the temp table to use in my in clause. However user defined functions in redshift do not allow tables as a return type. How are others solving this problem in redshift.
Thanks
I was able to create a stored procedure that takes a varchar and creates a temp table of all "slices" of the varchar broken up by a delimiter (in this case a ','). I just wanted to share it here in case someone else has this issue.
Here is the procedure:
CREATE OR REPLACE Procedure sp_UPCStringToTempTable(upcList IN varchar(max))
AS 'DECLARE
idx int;
slice varchar(8000);
upcListVar varchar(max);
BEGIN
idx = 1;
upcListVar = upcList;
DROP TABLE if exists tmp_upc;
CREATE TEMP TABLE tmp_upc(upc varchar(14));
WHILE idx != 0 LOOP
idx = charindex('','', upcListVar);
IF idx != 0 THEN
slice = left(upcListVar, idx - 1);
END IF;
IF idx = 0 THEN
slice = upcListVar;
END IF;
IF len(slice) > 0 THEN
INSERT INTO tmp_upc values (slice);
END IF;
upcListVar = right(upcListVar, len(upcListVar) - idx);
END LOOP;
END;
' LANGUAGE plpgsql;
create table num(id int) ;
insert into num values(1), (2),(3);
with t as
(
select split_part('0000000004011, 0000000094011',',',id ) col1 from num
)
select * from a join t on a.col1 = t.col1
This should solve your problem.

Executing prepared SQL in function without loop returning a table

I have this source here:
CREATE OR REPLACE FUNCTION SWITCHTEST (CHOICE VARCHAR(10))
RETURNS TABLE ( R_COL1 VARCHAR(1024) ,R_COL2 VARCHAR(1024) )
LANGUAGE SQL
MODIFIES SQL DATA
BEGIN
DECLARE SQLSTATE CHAR(5);
DECLARE SELECT1 VARCHAR(1024);
DECLARE L_COL1 VARCHAR(1024);
DECLARE L_COL2 VARCHAR(1024);
SET SELECT1 = 'SELECT TEST, DESCR FROM TESTTAB FETCH FIRST 10 ROWS ONLY';
SET SELECT2 = 'SELECT DESCR, COLOUR FROM TESTTAB FETCH FIRST 10 ROWS ONLY';
IF CHOICE = 'FIRST' THEN
PREPARE S1 FROM SELECT1;
ELSEIF CHOICE = 'SECOND' THEN
PREPARE S1 FROM SELECT2;
ELSE
END IF;
RETURN EXEC(S1);
END#
Calling it like
SELECT * FROM TABLE (SWITCHTEST('FIRST')) #
It should just execute the SQL in the S1 prepared statement and I don't want to use a loop in the function.
I am running DB2 Windows 10.5
Any ideas on how to fix this?
I know that the EXEC(S1) is wrong but I can't find anything on the IBM page that shows how to make this work.
Thank you for your help.
Viking
Using intermediate CREATED GLOBAL TEMPORARY TABLE.
--#SET TERMINATOR #
CREATE GLOBAL TEMPORARY TABLE SWITCHTEST ( R_COL1 VARCHAR(1024) ,R_COL2 VARCHAR(1024)) ON COMMIT PRESERVE ROWS NOT LOGGED#
CREATE OR REPLACE FUNCTION SWITCHTEST (CHOICE VARCHAR(10))
RETURNS TABLE (R_COL1 VARCHAR(1024), R_COL2 VARCHAR(1024) )
LANGUAGE SQL
MODIFIES SQL DATA
BEGIN ATOMIC
DELETE FROM SWITCHTEST;
IF CHOICE='FIRST' THEN
INSERT INTO SWITCHTEST SELECT TEST, DESCR FROM TESTTAB FETCH FIRST 10 ROWS ONLY;
ELSEIF CHOICE='SECOND' THEN
INSERT INTO SWITCHTEST SELECT DESCR, COLOUR FROM TESTTAB FETCH FIRST 10 ROWS ONLY;
END IF;
RETURN SELECT R_COL1, R_COL2 FROM SWITCHTEST;
END#
Not sure if it fits your needs, but you can use a case expression to choose columns for the result table. I don't think fetch first is supported in functions, but you can add that in the call:
CREATE OR REPLACE FUNCTION SWITCHTEST (CHOICE VARCHAR(10))
RETURNS TABLE ( R_COL1 VARCHAR(1024) ,R_COL2 VARCHAR(1024) )
LANGUAGE SQL
READS SQL DATA
RETURN
SELECT CASE CHOICE WHEN 'FIRST' THEN TEST ELSE DESCR END
, CASE CHOICE WHEN 'FIRST' THEN DESCR ELSE COLOUR END
FROM TESTTAB ;
SELECT * FROM TABLE (SWITCHTEST('FIRST'))
FETCH FIRST 10 ROWS ONLY;

Store result of a query inside a function

I've the following function:
DO
$do$
DECLARE
maxgid integer;
tableloop integer;
obstacle geometry;
simplifyedobstacle geometry;
BEGIN
select max(gid) from public.terrain_obstacle_temp into maxgid;
FOR tableloop IN 1 .. maxgid
LOOP
insert into public.terrain_obstacle (tse_coll,tse_height,geom) select tse_coll,tse_height,geom
from public.terrain_obstacle_temp where gid = tableloop;
END LOOP;
END
$do$;
I need to modify this function in order to execute different queries according to the type of a column of public.terrain_obstacle_temp.
This is a temporary table created by reading a shapefile, and I need to know the kind of the geom column of that table. I have a query that give the data to me:
SELECT type
FROM geometry_columns
WHERE f_table_schema = 'public'
AND f_table_name = 'terrain_obstacle'
and f_geometry_column = 'geom';
It returns me a character_varying value (in this case MULTIPOLYGON).
Ho can I modify the function in order to get the result of the query, and create an if statement that allows me to execute some code according to the result of that query?
Is the intention to copy all the records from the temp table to the actual table? If so, you may be able to skip the loop:
insert into public.terrain_obstacle (tse_coll, tse_height, geom)
select tse_coll, tse_height, geom
from public.terrain_obstacle_temp
;
Do terrain_obstacle and terrain_obstacle_temp have the same structure? If so, then the "insert into ... select ..." should work fine provided the column types are the same.
If conditional typing is required, use the CASE WHEN syntax:
v_type geometry_columns.type%TYPE;
...
SELECT type
INTO v_type
FROM geometry_columns
WHERE f_table_schema = 'public'
AND f_table_name = 'terrain_obstacle'
AND f_geometry_column = 'geom'
;
insert into public.terrain_obstacle (tse_coll, tse_height, geom)
select tse_coll
,tse_height
,CASE WHEN v_type = 'MULTIPOLYGON' THEN my_func1(geom)
WHEN v_type = 'POINT' THEN my_func2(geom)
ELSE my_default(geom)
END
from public.terrain_obstacle_temp
;

value limitation in an IN clause Oracle

I work for a company that has a DW - ETL setup. I need to write a query that looks for over 2500+ values in an WHEN - IN clause and also over 1000+ values in a WHERE - IN clause. Basically it would look like the following:
SELECT
,user_id
,CASE WHEN user_id IN ('user_n', +2500 user_[n+1] ) THEN 1
ELSE 0
,item_id
FROM user_table
WHERE item_id IN ('item_n', +1000 item_[n+1] );
As you probably already know PL/SQL allows a maximum of 1000 values in an IN clause, so I tried adding OR - IN clauses (as suggested in other stackoverflow threads):
SELECT
,user_id
,CASE WHEN user_id IN ('user_n', +999 user_[n+1] )
OR user_id IN ('user_n', +999 user_[n+1] )
OR user_id IN ('user_n', +999 user_[n+1] ) THEN 1
ELSE 0 END AS user_group
,item_id
FROM user_table
WHERE item_id IN ('item_n', +999 item_[n+1] )
OR item_id IN ('item_n', +999 item_[n+1] );
NOTE: i know the math is erroneous in the examples above, but you get the point
The problem is that queries have a maximum executing time of 120 minutes and the job is being automatically killed. So I googled what solutions I could find and it seems Temporary Tables could be the solution I'm looking for, but with all honesty none of the examples I found is clear enough on how to include the values I want in the table and also how to use this table in my original query. Not even the ORACLE documentation was of much help.
Another potential problem is that I have limited rights and I've seen other people mention that in their companies they don't have the rights to create temporary tables.
Some of the info I found in my research:
ORACLE documentation
StackOverflow thread
[StackOverflow thread 2]
Another solution I found was using tuples instead, as mentioned in THIS thread (which I haven't tried) because as another user mentions performance seems greatly affected.
Any guidance on how to use a Temporary Table or if anyone has another way of dealing with this limitation would be greatly appreciated.
Create a global temporary table so no undo logs are created
CREATE GLOBAL TEMPORARY TABLE <table_name> (
<column_name> <column_data_type>,
<column_name> <column_data_type>,
<column_name> <column_data_type>)
ON COMMIT DELETE ROWS;
then depending on how the user list arrives import the data into a holding table and then run
select 'INSERT INTO global_temporary_table <column> values '
|| holding_table.column
||';'
FROM holding_table.column;
This gives you insert statements as output which you run to insert the data.
then
SELECT <some_column>
FROM <some_table>
WHERE <some_value> IN
(SELECT <some_column> from <global_temporary_table>
Use a collection:
CREATE TYPE Ints_Table AS TABLE OF INT;
CREATE TYPE IDs_Table AS TABLE OF CHAR(5);
Something like this:
SELECT user_id,
CASE WHEN user_id MEMBER OF Ints_Table( 1, 2, 3, /* ... */ 2500 )
THEN 1
ELSE 0
END
,item_id
FROM user_table
WHERE item_id MEMBER OF IDs_table( 'ABSC2', 'DITO9', 'KMKM9', /* ... */ 'QD3R5' );
Or you can use PL/SQL to populate a collection:
VARIABLE cur REFCURSOR;
DECLARE
t_users Ints_Table;
t_items IDs_Table;
f UTL_FILE.FILE_TYPE;
line VARCHAR2(4000);
BEGIN
t_users.EXTEND( 2500 );
FOR i = 1 .. 2500 LOOP
t_users( t_users.COUNT ) := i;
END LOOP;
// load data from a file
f := UTL_FILE.FOPEN('DIRECTORY_HANDLE','datafile.txt','R');
IF UTL_FILE.IS_OPEN(f) THEN
LOOP
UTL_FILE.GET_LINE(f,line);
IF line IS NULL THEN EXIT; END IF;
t_items.EXTEND;
t_items( t_items.COUNT ) := line;
END LOOP;
OPEN :cur FOR
SELECT user_id,
CASE WHEN user_id MEMBER OF t_users
THEN 1
ELSE 0
END
,item_id
FROM user_table
WHERE item_id MEMBER OF t_items;
END;
/
PRINT cur;
Or if you are using another language to call the query then you could pass the collections as a bind value (as shown here).
In PL/SQL you could use a collection type. You could create your own like this:
create type string_table is table of varchar2(100);
Or use an existing type such as SYS.DBMS_DEBUG_VC2COLL which is a table of VARCHAR2(1000).
Now you can declare a collection of this type for each of your lists, populate it, and use it in the query - something like this:
declare
strings1 SYS.DBMS_DEBUG_VC2COLL := SYS.DBMS_DEBUG_VC2COLL();
strings2 SYS.DBMS_DEBUG_VC2COLL := SYS.DBMS_DEBUG_VC2COLL();
procedure add_string1 (p_string varchar2) is
begin
strings1.extend();
strings1(strings.count) := p_string;
end;
procedure add_string2 (p_string varchar2) is
begin
strings2.extend();
strings2(strings2.count) := p_string;
end;
begin
add_string1('1');
add_string1('2');
add_string1('3');
-- and so on...
add_string1('2500');
add_string2('1');
add_string2('2');
add_string2('3');
-- and so on...
add_string2('1400');
for r in (
select user_id
, case when user_id in table(strings2) then 1 else 0 end as indicator
, item_id
from user_table
where item_id in table(strings1)
)
loop
dbms_output.put_Line(r.user_id||' '||r.indicator);
end loop;
end;
/
You can use below example to understand Global temporary tables and the type of GTT.
CREATE GLOBAL TEMPORARY TABLE GTT_PRESERVE_ROWS (ID NUMBER) ON COMMIT PRESERVE ROWS;
INSERT INTO GTT_PRESERVE_ROWS VALUES (1);
COMMIT;
SELECT * FROM GTT_PRESERVE_ROWS;
DELETE FROM GTT_PRESERVE_ROWS;
COMMIT;
TRUNCATE TABLE GTT_PRESERVE_ROWS;
DROP TABLE GTT_PRESERVE_ROWS;--WONT WORK IF YOU DIDNOT TRUNCATE THE TABLE OR THE TABLE IS BEING USED IN SOME OTHER SESSION
CREATE GLOBAL TEMPORARY TABLE GTT_DELETE_ROWS (ID NUMBER) ON COMMIT DELETE ROWS;
INSERT INTO GTT_DELETE_ROWS VALUES (1);
SELECT * FROM GTT_DELETE_ROWS;
COMMIT;
SELECT * FROM GTT_DELETE_ROWS;
DROP TABLE GTT_DELETE_ROWS;
However as you mentioned you receive the input in an excel file so you can simply create a table and load data in that table. Once the data is loaded you can use the data in IN clause of your query.
select * from employee where empid in (select empid from temptable);
create temporary table userids (userid int);
insert into userids(...)
then a join or in subquery
select ...
where user_id in (select userid from userids);
drop temporary table userids;

foreach rows of my table and update my ROW_NUMBER column

I would like to create a script pl/sql where I can modify the value of my column ROW_NUMBER (the first time the value of ROW_NUMBER equal NULL).
This is the structure of my table 'A' :
CREATE TABLE A
(
"NAME" VARCHAR2(25 BYTE),
"NUM" NUMBER(10,0)
)
I would like to foreach all rows of table A and increment my Column 'NUM' by 1 if Column 'NAME' equal 'DEB'.
I would like to get the result like :
I created one pl/sql script :
DECLARE
INcrmt NUMBER(4):=1;
line WORK_ODI.TEST_SEQ%ROWTYPE;--before fetch it returns 0
CURSOR c_select IS
SELECT ROW_NUMBER,VALUE FROM WORK_ODI.TEST_SEQ;
BEGIN
OPEN c_select;
LOOP
FETCH c_select INTO line;
DBMS_OUTPUT.PUT_LINE(line.VALUE);
if line.VALUE like '%DEB%'
then
UPDATE WORK_ODI.TEST_SEQ SET ROW_NUMBER = INcrmt WHERE VALUE=line.VALUE;
INcrmt := INcrmt + 1;
end if;
if line.VALUE not like '%DEB%'
then
UPDATE WORK_ODI.TEST_SEQ SET ROW_NUMBER = INcrmt WHERE VALUE=line.VALUE;
end if;
EXIT WHEN c_select%NOTFOUND;
END LOOP;
CLOSE c_select;
COMMIT;
END;
DECLARE
INcrmt NUMBER(4):=1;
line WORK_ODI.TEST_SEQ%ROWTYPE;--before fetch it returns 0
CURSOR c_select IS
SELECT ROW_NUMBER,VALUE FROM WORK_ODI.TEST_SEQ;
BEGIN
OPEN c_select;
LOOP
FETCH c_select INTO line;
DBMS_OUTPUT.PUT_LINE(line.VALUE);
if line.VALUE like '%DEB%'
then
UPDATE WORK_ODI.TEST_SEQ SET ROW_NUMBER = INcrmt WHERE VALUE=line.VALUE;
INcrmt := INcrmt + 1;
end if;
if line.VALUE not like '%DEB%'
then
UPDATE WORK_ODI.TEST_SEQ SET ROW_NUMBER = INcrmt WHERE VALUE=line.VALUE;
end if;
EXIT WHEN c_select%NOTFOUND;
END LOOP;
CLOSE c_select;
COMMIT;
END;
but this is not work well , please take a look at what it gives me as result :
please anybody can help me
First, you should have an Aid column of some sort. In Oracle 12+, you can use an identity. In earlier versions, you can use a sequence. This provides an ordering for the rows in the table, based on insert order.
Second, you can do what you want on output:
select a.*,
sum(case when a.name like 'DEB%' then 1 else 0 end) over (order by aid) as row_number
from a;
If you really need to keep the values in the table, then you can use a merge statement to assign values to existing rows (the aid column is very handy for this). You will need a trigger afterwards to maintain it.
My suggestion is to do the calculation on the data, rather than storing the value in the data. Maintaining the values with updates and deletes seems like a real pain.