How to insert rows from one Snowflake table to another without hardcoding the column names - insert

I have a Snowflake table A as below:
CREATE or replace table A
(
Address VARCHAR(50),
Zip VARCHAR(50));
insert into A (Address, Zip) values
('ABC', '20987'),
('XYZ', '20989'),
('CBZ', '20980');
I have an empty Snowflake table B as below
CREATE or replace table B
(
Address VARCHAR(50),
Zip VARCHAR(50),
State VARCHAR(50));
Now I want to insert rows from Table A to Table B exactly like below
insert into B (Address, Zip)
select Address, Zip from A
but instead of hardcoding the column names, I want to get it from information_schema as below
select COLUMN_NAME
from db.information_schema.columns
where table_schema = 'schema' and TABLE_NAME = 'A'

If you're using an insert from a select and don't want to list out the column names, just remove the column names and put NULL where there's nothing else to put from the select.
Change this:
insert into B (Address, Zip)
select Address, Zip from A
To this:
insert into B
select Address, Zip, NULL from A
If you have a huge number of columns and want a labor-saving approach, the age-old DBA technique of a SQL generator will work fine. Something like this:
select 'insert into b select ' || (select listagg(COLUMN_NAME,',')
within group (order by ORDINAL_POSITION) COLS
from information_schema.columns
where TABLE_SCHEMA = 'PUBLIC' and TABLE_NAME = 'A') || ' from A;'
;
You can then add nulls where required because table B has columns table A does not, or the generator can get more sophisticated and map the columns between the table filling in nulls where a column exists in B but not A.

Actual code using Snowflake Scripting and parameters. Matching based on column names on target and source. The generated code is executed using EXECUTE IMMEDIATE and actual statement is returned for debug.
DECLARE
trg_table_schema TEXT := 'PUBLIC';
trg_table_name TEXT := 'B';
src_table_schema TEXT := 'PUBLIC';
src_table_name TEXT := 'A';
sql TEXT;
BEGIN
WITH trg AS (
SELECT *
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = :trg_table_name
AND TABLE_SCHEMA = :trg_table_schema
), src AS (
SELECT *
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = :src_table_name
AND TABLE_SCHEMA = :src_table_schema
)
SELECT
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
'INSERT INTO <trg_table_schema>.<trg_table_name>(<col_list>)
SELECT <col_list> FROM <src_table_schema>.<src_table_name>;'
, '<trg_table_schema>', :trg_table_schema)
, '<trg_table_name>', :trg_table_name)
, '<col_list>', LISTAGG(src.COLUMN_NAME, ','))
, '<src_table_schema>', :src_table_schema)
, '<src_table_name>', :src_table_name)
INTO :sql
FROM trg
JOIN src ON trg.COLUMN_NAME = src.COLUMN_NAME;
EXECUTE IMMEDIATE :sql;
RETURN :sql;
END;
Output:
The key part is main query which works by taking matching(by name) columns from source and target tables.
INSERT INTO <trg_table_schema>.<trg_table_name>(<col_list>)
SELECT <col_list> FROM <src_table_schema>.<src_table_name>;

You can write a stored procedure to generate and execute the INSERT statement.
https://docs.snowflake.com/en/sql-reference/stored-procedures-usage.html#stored-procedure-considerations
https://docs.snowflake.com/en/sql-reference/sql/execute-immediate.html

Related

Can we use an insert overwrite after using insert all

In Snowflake I am trying to insert updated records to a table. Then I want to identify the records that were just inserted as the most recent records save that as the final table output in a new column called ACTIVE which will either be true or flase. I am having an issue incorporating some sort of updated table segment to my current query. I need everything be contained in the same query rather than break it up into separate parts.
I have my table as follows
CREATE TABLE IF NOT EXISTS MY_TABLE
(
LINK_ID BINARY NOT NULL,
LOAD TIMESTAMP NOT NULL,
SOURCE STRING NOT NULL,
SOURCE_DATE TIMESTAMP NOT NULL,
ORDER BIGINT NOT NULL,
ID BINARY NOT NULL,
ATTRIBUTE_ID BINARY NOT NULL
);
I have records being inserted in this way:
INSERT ALL
WHEN HAS_DATA AND ID_SEQ_NUM > 1 AND (SELECT COUNT(1) FROM MY_TABLE WHERE ID = KEY) = 0 THEN
INTO MY_TABLE VALUES (
LINK_KEY,
TIME,
DATASET_NAME,
DATASET_DATE,
ORDER_NUMBER,
O_KEY,
OA_KEY
)
SELECT *
FROM TEST_TABLE;
I would like my final table from this to be the output as
SELECT *, ORDER != MAX(ORDER) OVER (PARTITION BY ID) AS ACTIVE
FROM MY_TABLE;
This is so I can identify the most recent record per ID group as ACTIVE/TRUE and the previous records within that ID group as INACTIVE/FALSE
I tried to use an insert overwrite method like this
INSERT ALL
WHEN HAS_DATA AND ID_SEQ_NUM > 1 AND (SELECT COUNT(1) FROM MY_TABLE WHERE ID = KEY) = 0 THEN
INTO MY_TABLE VALUES (
LINK_KEY,
TIME,
DATASET_NAME,
DATASET_DATE,
ORDER_NUMBER,
O_KEY,
OA_KEY
)
INSERT OVERWRITE INTO MY_TABLE
SELECT *, RSRC_OFFSET != MAX(RSRC_OFFSET) OVER (PARTITION BY ID) AS ACTIVE
FROM L_OPTION_OPTION_ALLOCATION_TEST
SELECT *
FROM MY_TABLE;
However, it seems the insert overwrite doesn't work in this way (also I am not sure if I can just add a new column to the table like this?). Is there a way I can incorporate it into this query or a different way to update the table with this new ACTIVE column within this query itself?
Also I am using INSERT ALL here because I actually have multiple different tables I am inserting into at once, but this is the current table that I am trying to modify.
You can use the overwrite option with conditional multi-table inserts.
Starting with your current statement:
INSERT ALL
WHEN HAS_DATA AND ID_SEQ_NUM > 1 AND (SELECT COUNT(1) FROM MY_TABLE WHERE ID = KEY) = 0 THEN
INTO MY_TABLE VALUES (
LINK_KEY,
TIME,
DATASET_NAME,
DATASET_DATE,
ORDER_NUMBER,
O_KEY,
OA_KEY
)
SELECT *
FROM TEST_TABLE;
Add the overwrite option immediately after the insert command:
INSERT OVERWRITE ALL
WHEN HAS_DATA AND ID_SEQ_NUM > 1 AND (SELECT COUNT(1) FROM MY_TABLE WHERE ID = KEY) = 0 THEN
INTO MY_TABLE VALUES (
LINK_KEY,
TIME,
DATASET_NAME,
DATASET_DATE,
ORDER_NUMBER,
O_KEY,
OA_KEY
)
SELECT *
FROM TEST_TABLE;
Note that this will truncate and insert ALL tables in the multi-table insert. There is not a way to be selective about which tables get truncated and inserted and which don't.
https://docs.snowflake.com/en/sql-reference/sql/insert-multi-table.html#optional-parameters

How to Update value in one table by coping it from another table in plsql ?

This is a simple example of what I need to do. In fact I want to Update value in one table by coping it from another table by using cursors in plsql.
I take table f and table b as two examples:
f=
1|Thom
2|Bob
3|Steven
5|Arthur
b=
7|Nataly
9|Alfred
, where I need to insert the b 's tow lines in first f two lines:
create table f (a number, b varchar2(10));
insert into f values (1,'Thom');
insert into f values (2,'Bob');
insert into f values (3,'Steven');
insert into f values (5,'Arthur');
commit;
create table b (c number, d varchar2(10));
insert into b values (7,'Nataly');
insert into b values (9,'Alfred');
commit;
create or replace procedure wco as
cursor c_f is
select a,b from f for update;
v_a f.a%type;
v_b f.b%type;
cursor c_b is
select c,d from b;
v_c b.c%type;
v_d b.d%type;
begin
open c_f;
open c_b
loop
fetch c_f into v_a, v_b;
exit when c_f%ROWCOUNT=c_b%RROWCOUNT;
update f set a=v_c and b=v_d where current of c_f;
end loop;
close c_d:
close c_f;
end;
/
exec wco;
select * from f;
drop table f;
The expected result (What i hope to have):
7|Nataly
9|Alfred
3|Steven
5|Arthur
But what I have now (as a result)is:
1|Thom
2|Bob
3|Steven
5|Arthur
How do I resolve this problem, I am a beginner with PLSQL, I would be very grateful if you could help me please.
Your procedure compiles, but with errors;
In order to see compilation errors
run show errors; after compilation.
Change open c_b to open c_b; (missing semicolon at the end)
Change close c_d: to close c_b; (correct name is c_b and semicolon instead of colon)
Change exit when c_f%ROWCOUNT=c_b%RROWCOUNT; to exit when c_f%ROWCOUNT=c_b%ROWCOUNT; (wrong %RROWCOUNT;)
update f set a=v_c and b=v_d where current of c_f; This is wrong SQL syntax.
Should be update table_name set column=value, column=value;
It is important to provide descriptive names to your variables, so it would be easier to understand your logic.
As I understood you want to copy only second two rows from source table.
create table source_table (
id number,
name varchar2(10));
/
insert into source_table values (1,'Thom');
insert into source_table values (2,'Bob');
insert into source_table values (3,'Steven');
insert into source_table values (5,'Arthur');
/
create table target_table (
id number,
name varchar2(10));
/
insert into target_table values (7,'Nataly');
insert into target_table values (9,'Alfred');
/
create or replace procedure copy_tables
AS
begin
FOR source_row IN (select id, name
from source_table
offset 2 rows)
LOOP
insert into target_table
values(source_row.id, source_row.name);
END LOOP;
end;
/
exec copy_tables;
/
select id, name from target_table;
/
You don't need PL/SQL for this; it can be achieved in a single MERGE statement:
merge into f tgt
using (select coalesce(b1.id, f1.id) id,
coalesce(b1.name, f1.name) name,
f1.f_rowid
from (select id,
name,
rowid f_rowid,
row_number() over (order by id) rn
from f) f1
full outer join (select id,
name,
row_number() over (order by id) rn
from b) b1
on f1.rn = b1.rn) src
on (tgt.rowid = src.f_rowid)
when matched then
update set tgt.id = src.id,
tgt.name = src.name
where tgt.id != src.id
or tgt.name != src.name
when not matched then
insert (tgt.id, tgt.name)
values (src.id, src.name);
See https://livesql.oracle.com/apex/livesql/file/content_FBPR7YCLFVWO7NGDXTLSP1R97.html for the test details (two examples; the target table has more and has fewer rows than the source table).
If you need to, you could add the above insert into a procedure, e.g.:
create procedure populate_target_table as
begin
merge into f tgt
using (select coalesce(b1.id, f1.id) id,
coalesce(b1.name, f1.name) name,
f1.f_rowid
from (select id,
name,
rowid f_rowid,
row_number() over (order by id) rn
from f) f1
full outer join (select id,
name,
row_number() over (order by id) rn
from b) b1
on f1.rn = b1.rn) src
on (tgt.rowid = src.f_rowid)
when matched then
update set tgt.id = src.id,
tgt.name = src.name
where tgt.id != src.id
or tgt.name != src.name
when not matched then
insert (tgt.id, tgt.name)
values (src.id, src.name);
end;
/

Delete records from source table after those has been transferred via stored procedure to target table

this is my stored procedure which i got with kindly help by user75ponic generated.
CREATE OR REPLACE PROCEDURE ARCHIVE_DATA
AS
BEGIN
MERGE
INTO Schema2.table trg
USING (
SELECT
column1
, column2
, column3
FROM
Schema1.table
WHERE
col_date >= 360
)
src
ON
(
trg.column1 = src.column1
)
WHEN NOT MATCHED THEN
INSERT
(
column1
, column2
, column3
)
VALUES
(
src.column1
, src.column2
, src.column3
)
;
COMMIT;
END;
/
Now this stored proc is supposed to transfer records from source to target table and it is doing so.
My challenge is to include the below delete script into this stored procedure. The purpose is to delete the records from source table after they have been moved, but only if they are present in the target table.
Below is the delete script:
DELETE FROM src WHERE src.table_date <= SYSDATE - 360 AND src.table_id IN (SELECT table_id FROM trg.column);
as reference this is the original post: original Post
After doing the insert, you can append the below DELETE statement to your procedure.
DELETE FROM schema1.src_table src
WHERE src.table_id IN (SELECT trg.table_id
FROM schema2.trg_table trg
WHERE trg.table_id = src.table_id);

Oracle Equivalent to MySQL INSERT IGNORE?

I need to update a query so that it checks that a duplicate entry does not exist before insertion. In MySQL I can just use INSERT IGNORE so that if a duplicate record is found it just skips the insert, but I can't seem to find an equivalent option for Oracle. Any suggestions?
If you're on 11g you can use the hint IGNORE_ROW_ON_DUPKEY_INDEX:
SQL> create table my_table(a number, constraint my_table_pk primary key (a));
Table created.
SQL> insert /*+ ignore_row_on_dupkey_index(my_table, my_table_pk) */
2 into my_table
3 select 1 from dual
4 union all
5 select 1 from dual;
1 row created.
Check out the MERGE statement. This should do what you want - it's the WHEN NOT MATCHED clause that will do this.
Do to Oracle's lack of support for a true VALUES() clause the syntax for a single record with fixed values is pretty clumsy though:
MERGE INTO your_table yt
USING (
SELECT 42 as the_pk_value,
'some_value' as some_column
FROM dual
) t on (yt.pk = t.the_pke_value)
WHEN NOT MATCHED THEN
INSERT (pk, the_column)
VALUES (t.the_pk_value, t.some_column);
A different approach (if you are e.g. doing bulk loading from a different table) is to use the "Error logging" facility of Oracle. The statement would look like this:
INSERT INTO your_table (col1, col2, col3)
SELECT c1, c2, c3
FROM staging_table
LOG ERRORS INTO errlog ('some comment') REJECT LIMIT UNLIMITED;
Afterwards all rows that would have thrown an error are available in the table errlog. You need to create that errlog table (or whatever name you choose) manually before running the insert using DBMS_ERRLOG.CREATE_ERROR_LOG.
See the manual for details
I don't think there is but to save time you can attempt the insert and ignore the inevitable error:
begin
insert into table_a( col1, col2, col3 )
values ( 1, 2, 3 );
exception when dup_val_on_index then
null;
end;
/
This will only ignore exceptions raised specifically by duplicate primary key or unique key constraints; everything else will be raised as normal.
If you don't want to do this then you have to select from the table first, which isn't really that efficient.
Another variant
Insert into my_table (student_id, group_id)
select distinct p.studentid, g.groupid
from person p, group g
where NOT EXISTS (select 1
from my_table a
where a.student_id = p.studentid
and a.group_id = g.groupid)
or you could do
Insert into my_table (student_id, group_id)
select distinct p.studentid, g.groupid
from person p, group g
MINUS
select student_id, group_id
from my_table
A simple solution
insert into t1
select from t2
where not exists
(select 1 from t1 where t1.id= t2.id)
This one isn't mine, but came in really handy when using sqlloader:
create a view that points to your table:
CREATE OR REPLACE VIEW test_view
AS SELECT * FROM test_tab
create the trigger:
CREATE OR REPLACE TRIGGER test_trig
INSTEAD OF INSERT ON test_view
FOR EACH ROW
BEGIN
INSERT INTO test_tab VALUES
(:NEW.id, :NEW.name);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN NULL;
END test_trig;
and in the ctl file, insert into the view instead:
OPTIONS(ERRORS=0)
LOAD DATA
INFILE 'file_with_duplicates.csv'
INTO TABLE test_view
FIELDS TERMINATED BY ','
(id, field1)
How about simply adding an index with whatever fields you need to check for dupes on and say it must be unique? Saves a read check.
yet another "where not exists"-variant using dual...
insert into t1(id, unique_name)
select t1_seq.nextval, 'Franz-Xaver' from dual
where not exists (select 1 from t1 where unique_name = 'Franz-Xaver');

Oracle: how to INSERT if a row doesn't exist

What is the easiest way to INSERT a row if it doesn't exist, in PL/SQL (oracle)?
I want something like:
IF NOT EXISTS (SELECT * FROM table WHERE name = 'jonny') THEN
INSERT INTO table VALUES ("jonny", null);
END IF;
But it's not working.
Note: this table has 2 fields, say, name and age. But only name is PK.
INSERT INTO table
SELECT 'jonny', NULL
FROM dual -- Not Oracle? No need for dual, drop that line
WHERE NOT EXISTS (SELECT NULL -- canonical way, but you can select
-- anything as EXISTS only checks existence
FROM table
WHERE name = 'jonny'
)
Assuming you are on 10g, you can also use the MERGE statement. This allows you to insert the row if it doesn't exist and ignore the row if it does exist. People tend to think of MERGE when they want to do an "upsert" (INSERT if the row doesn't exist and UPDATE if the row does exist) but the UPDATE part is optional now so it can also be used here.
SQL> create table foo (
2 name varchar2(10) primary key,
3 age number
4 );
Table created.
SQL> ed
Wrote file afiedt.buf
1 merge into foo a
2 using (select 'johnny' name, null age from dual) b
3 on (a.name = b.name)
4 when not matched then
5 insert( name, age)
6* values( b.name, b.age)
SQL> /
1 row merged.
SQL> /
0 rows merged.
SQL> select * from foo;
NAME AGE
---------- ----------
johnny
If name is a PK, then just insert and catch the error. The reason to do this rather than any check is that it will work even with multiple clients inserting at the same time. If you check and then insert, you have to hold a lock during that time, or expect the error anyway.
The code for this would be something like
BEGIN
INSERT INTO table( name, age )
VALUES( 'johnny', null );
EXCEPTION
WHEN dup_val_on_index
THEN
NULL; -- Intentionally ignore duplicates
END;
I found the examples a bit tricky to follow for the situation where you want to ensure a row exists in the destination table (especially when you have two columns as the primary key), but the primary key might not exist there at all so there's nothing to select.
This is what worked for me:
MERGE INTO table1 D
USING (
-- These are the row(s) you want to insert.
SELECT
'val1' AS FIELD_A,
'val2' AS FIELD_B
FROM DUAL
) S ON (
-- This is the criteria to find the above row(s) in the
-- destination table. S refers to the rows in the SELECT
-- statement above, D refers to the destination table.
D.FIELD_A = S.FIELD_A
AND D.FIELD_B = S.FIELD_B
)
-- This is the INSERT statement to run for each row that
-- doesn't exist in the destination table.
WHEN NOT MATCHED THEN INSERT (
FIELD_A,
FIELD_B,
FIELD_C
) VALUES (
S.FIELD_A,
S.FIELD_B,
'val3'
)
The key points are:
The SELECT statement inside the USING block must always return rows. If there are no rows returned from this query, no rows will be inserted or updated. Here I select from DUAL so there will always be exactly one row.
The ON condition is what sets the criteria for matching rows. If ON does not have a match then the INSERT statement is run.
You can also add a WHEN MATCHED THEN UPDATE clause if you want more control over the updates too.
Using parts of #benoit answer, I will use this:
DECLARE
varTmp NUMBER:=0;
BEGIN
-- checks
SELECT nvl((SELECT 1 FROM table WHERE name = 'john'), 0) INTO varTmp FROM dual;
-- insert
IF (varTmp = 1) THEN
INSERT INTO table (john, null)
END IF;
END;
Sorry for I don't use any full given answer, but I need IF check because my code is much more complex than this example table with name and age fields. I need a very clear code. Well thanks, I learned a lot! I'll accept #benoit answer.
In addition to the perfect and valid answers given so far, there is also the ignore_row_on_dupkey_index hint you might want to use:
create table tq84_a (
name varchar2 (20) primary key,
age number
);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Johnny', 77);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Pete' , 28);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Sue' , 35);
insert /*+ ignore_row_on_dupkey_index(tq84_a(name)) */ into tq84_a values ('Johnny', null);
select * from tq84_a;
The hint is described on Tahiti.
you can use this syntax:
INSERT INTO table_name ( name, age )
select 'jonny', 18 from dual
where not exists(select 1 from table_name where name = 'jonny');
if its open an pop for asking as "enter substitution variable" then use this before the above queries:
set define off;
INSERT INTO table_name ( name, age )
select 'jonny', 18 from dual
where not exists(select 1 from table_name where name = 'jonny');
You should use Merge:
For example:
MERGE INTO employees e
USING (SELECT * FROM hr_records WHERE start_date > ADD_MONTHS(SYSDATE, -1)) h
ON (e.id = h.emp_id)
WHEN MATCHED THEN
UPDATE SET e.address = h.address
WHEN NOT MATCHED THEN
INSERT (id, address)
VALUES (h.emp_id, h.address);
or
MERGE INTO employees e
USING hr_records h
ON (e.id = h.emp_id)
WHEN MATCHED THEN
UPDATE SET e.address = h.address
WHEN NOT MATCHED THEN
INSERT (id, address)
VALUES (h.emp_id, h.address);
https://oracle-base.com/articles/9i/merge-statement
CTE and only CTE :-)
just throw out extra stuff. Here is almost complete and verbose form for all cases of life. And you can use any concise form.
INSERT INTO reports r
(r.id, r.name, r.key, r.param)
--
-- Invoke this script from "WITH" to the end (";")
-- to debug and see prepared values.
WITH
-- Some new data to add.
newData AS(
SELECT 'Name 1' name, 'key_new_1' key FROM DUAL
UNION SELECT 'Name 2' NAME, 'key_new_2' key FROM DUAL
UNION SELECT 'Name 3' NAME, 'key_new_3' key FROM DUAL
),
-- Any single row for copying with each new row from "newData",
-- if you will of course.
copyData AS(
SELECT r.*
FROM reports r
WHERE r.key = 'key_existing'
-- ! Prevent more than one row to return.
AND FALSE -- do something here for than!
),
-- Last used ID from the "reports" table (it depends on your case).
-- (not going to work with concurrent transactions)
maxId AS (SELECT MAX(id) AS id FROM reports),
--
-- Some construction of all data for insertion.
SELECT maxId.id + ROWNUM, newData.name, newData.key, copyData.param
FROM copyData
-- matrix multiplication :)
-- (or a recursion if you're imperative coder)
CROSS JOIN newData
CROSS JOIN maxId
--
-- Let's prevent re-insertion.
WHERE NOT EXISTS (
SELECT 1 FROM reports rs
WHERE rs.name IN(
SELECT name FROM newData
));
I call it "IF NOT EXISTS" on steroids. So, this helps me and I mostly do so.

Resources