CREATE UNIQUE INDEX IF NOT EXISTS in postgreSQL - sql

Plese I would like to do in PostgreSQL something like
CREATE UNIQUE INDEX IF NOT EXISTS
Any idea?

You can check, if an index with a given name exists with this statement.
If your index name is some_table_some_field_idx
SELECT count(*) > 0
FROM pg_class c
WHERE c.relname = 'some_table_some_field_idx'
AND c.relkind = 'i';
Starting from Postgres 9.5 you can even use
CREATE INDEX IF NOT EXISTS

Just another ready-to-use solution.
PostgreSQL v9.0+:
DO $BLOCK$
BEGIN
BEGIN
CREATE INDEX index_name ON table_name( column_name );
EXCEPTION
WHEN duplicate_table
THEN RAISE NOTICE 'index ''index_name '' on table_name already exists, skipping';
END;
END;
$BLOCK$;
PostgreSQL v9.5+:
CREATE INDEX IF NOT EXISTS index_name ON table_name( column_name );

I have wrapped a_horse_with_no_name's code with PLSQL function for more convenient usage. I hope somebody will find it useful.
CREATE OR REPLACE FUNCTION create_index(table_name text, index_name text, column_name text) RETURNS void AS $$
declare
l_count integer;
begin
select count(*)
into l_count
from pg_indexes
where schemaname = 'public'
and tablename = lower(table_name)
and indexname = lower(index_name);
if l_count = 0 then
execute 'create index ' || index_name || ' on ' || table_name || '(' || column_name || ')';
end if;
end;
$$ LANGUAGE plpgsql;
usage:
select create_index('my_table', 'my_index_name', 'id');

You need some procedural code for this, something like this (untested!):
do
$$
declare
l_count integer;
begin
select count(*)
into l_count
from pg_indexes
where schemaname = 'public'
and tablename = 'your_table'
and indexname = 'your_index_name';
if l_count = 0 then
execute 'create unique index public.your_index_name on public.your_table(id)';
end if;
end;
$$

If you are still stuck in previous versions, I would recommend not using count, but just the query directly in your if condition. Makes the code simpler. You can try something like this:
do
$$
begin
if not exists (
select indexname
from pg_indexes
where schemaname = 'schemaname'
and tablename = 'tablename'
and indexname = 'indexname'
)
then
create unique indexname (...);
end if;
end
$$;

Another solution that support multiple columns index, based on #Kragh answer
CREATE or replace FUNCTION create_index(_index text, _table text, VARIA
DIC param_args text[]) RETURNS void AS
$$
declare
l_count integer;
begin
select count(*) into l_count
from pg_indexes
where schemaname = 'public'
and tablename = lower(_table)
and indexname = lower(_index);
if l_count = 0 then
EXECUTE format('create index %I on %I (%s)', _index, _table, array_to_string($3,','));
end if;
END;
$$
LANGUAGE plpgsql;
and then you can use it like any other pg function:
select create_index('events_timestamp_type_idx', 'events', 'timestamp', 'type');

Related

Use lateral join to loop over all tables from schema

I want to count certain values in all tables of a schema that contain a column that can contain those values.
Was hoping to use a LATERAL join to loop over all tables, but I'm running into issues:
select
fully_qualified_table_name,
cnt
from (
select
'datastore.' || table_name as fully_qualified_table_name
from
information_schema.columns
where
table_schema = 'datastore'
and column_name = 'dss_current_flag'
cross join lateral
select
count(*) as cnt
from
information_schema.fully_qualified_table_name
);
Is this possible?
I'm afraid it is not possible to run dynamic queries using pure SQL. You might wanna check PL/pgSQL instead, e.g.
CREATE OR REPLACE FUNCTION count_records()
RETURNS bigint AS $$
DECLARE
rec record;
res bigint = 0; ct bigint = 0;
BEGIN
FOR rec IN
SELECT table_schema AS sch,table_name AS tb
FROM information_schema.columns
WHERE table_schema = 'datastore' AND column_name = 'dss_current_flag'
LOOP
EXECUTE format($ex$ SELECT count(*) FROM %I.%I $ex$,rec.sch,rec.tb)
INTO ct;
res := res + ct;
END LOOP;
RETURN res;
END $$ LANGUAGE 'plpgsql';
The more flexible approach would be to provide schema and table names as parameters in the function call instead of hard coding it in the function body, such as CREATE FUNCTION count_records(_schema_name text, _table_name text) .., or even the fully qualified table name as a single parameter: CREATE FUNCTION count_records(_qualified_table_name text) ... .
Demo: db<>fiddle
Based on the answer by #jim-jones my final solution was
CREATE TYPE datastore.schema_table_column_counts_type AS (
schema_name text,
table_name text,
column_name text,
value text,
count_p bigint);
CREATE OR REPLACE FUNCTION datastore.count_records_in_schema_where_column_has_value(_schema_name text, _column_name text, _value text)
RETURNS setof datastore.schema_table_column_counts_type language plpgsql AS $$
DECLARE
rec record;
result_record datastore.schema_table_column_counts_type;
BEGIN
FOR rec IN
SELECT
table_schema AS sch,
table_name AS tb,
$2 as cn,
$3 as v
FROM information_schema.columns
WHERE table_schema = $1
AND column_name = $2
LOOP
EXECUTE format($ex$
SELECT
'%1$s' as schema_name,
'%2$s' as table_name,
'%3$s' as column_name,
'%4$s' as value,
count(*)
FROM
%1$s.%2$s
WHERE
%3$s = %4$L
$ex$
, rec.sch, rec.tb, rec.cn, rec.v)
INTO result_record;
return next result_record;
END LOOP;
END $$ ;
SELECT * from datastore.count_records_in_schema_where_column_has_value('datastore', 'dss_current_flag', 'P');

dynamic SQL ERROR: column "age" does not exist

postgres 12
I am trying to loop through a table which has schema , table_names and columns
I want to do various things like finding nulls ,row count etc. I failed at the first hurdle trying to update the col records.
table i am using
CREATE TABLE test.table_study (
table_schema text,
table_name text,
column_name text,
records int,
No_Nulls int,
No_Blanks int,
per_pop int
);
I populate the table with some schema names ,tables and columns from information_schema.columns
insert into test.table_study select table_schema, table_name, column_name
from information_schema.columns
where table_schema like '%white'
order by table_schema, table_name, ordinal_position;
I want to populate the rest with a function
function :-
CREATE OR REPLACE PROCEDURE test.insert_data_population()
as $$
declare s record;
declare t record;
declare c record;
BEGIN
FOR s IN SELECT distinct table_schema FROM test.table_study
LOOP
FOR t IN SELECT distinct table_name FROM test.table_study where table_schema = s.table_schema
loop
FOR c IN SELECT column_name FROM test.table_study where table_name = t.table_name
LOOP
execute 'update test.table_study set records = (select count(*) from ' || s.table_schema || '.' || t.table_name || ') where table_study.table_name = '|| t.table_name ||';';
END LOOP;
END LOOP;
END LOOP;
END;
$$
LANGUAGE plpgsql;
I get this error SQL Error [42703]: ERROR: column "age" does not exist. the table age does exist.
when I take out the where clause
execute 'update referralunion.testinsert ti set records = (select count(*) from ' || s.table_schema || '.' || t.table_name || ') ;';
it works, I just cant figure out whats wrong?
Your procedure is structured entirely wrong. What it results in is an attempt to get every column name for every table name in every schema. I would guess results in your column does not exist error. Further is shows procedural thinking. SQL requires think in terms of sets. Below I use basically your query to demonstrate then a revised version which uses a single loop.
-- setup (dropping schema references)
create table table_study (
table_schema text,
table_name text,
column_name text,
records int,
no_nulls int,
no_blanks int,
per_pop int
);
insert into table_study(table_schema, table_name, column_name)
values ('s1','t1','age')
, ('s2','t1','xyz');
-- procedure replacing EXECUTE with Raise Notice.
create or replace procedure insert_data_population()
as $$
declare
s record;
t record;
c record;
line int = 0;
begin
for s in select distinct table_schema from table_study
loop
for t in select distinct table_name from table_study where table_schema = s.table_schema
loop
for c in select column_name from table_study where table_name = t.table_name
loop
line = line+1;
raise notice '%: update table_study set records = (select count(*) from %.% where table_study.table_name = %;'
, line, s.table_schema, t.table_name, c.column_name;
end loop;
end loop;
end loop;
end;
$$
language plpgsql;
Run procedure
do $$
begin
call insert_data_population();
end;
$$;
RESULTS
1: update table_study set records = (select count(*) from s2.t1 where table_study.table_name = age; 2: update table_study set records = (select count(*) from s2.t1 where table_study.table_name = xyz; 3: update table_study set records = (select count(*) from s1.t1 where table_study.table_name = age; 4: update table_study set records = (select count(*) from s1.t1 where table_study.table_name = xyz;
Notice lines 2 and 3. Each references a column name that does not exist in the table. This results from the FOR structure with the same table name in different schema.
Revision for Single Select statement with Single For loop.
create or replace
procedure insert_data_population()
language plpgsql
as $$
declare
s record;
line int = 0;
begin
for s in select distinct table_schema, table_name, column_name from table_study
loop
line = line+1;
raise notice '%: update table_study set records = (select count(*) from %.% where table_study.table_name = %;'
, line, s.table_schema, s.table_name, s.column_name;
end loop;
end;
$$;
do $$
begin
call insert_data_population();
end;
$$;
RESULTS
1: update table_study set records = (select count(*) from s2.t1 where table_study.table_name = xyz;
2: update table_study set records = (select count(*) from s1.t1 where table_study.table_name = age;
Note: In Postgres DECLARE begins a block. It is not necessary to declared each variable. I would actually consider it bad practice. In theory it could require an end for each declare as each could be considered a nested block. Fortunately Postgres does not require this.

Select from all tables inside the schema containing column with name

How can I get select (table_name, table_name.age)?
I need to get values from column 'age' from all tables having this column/
I have this function
CREATE OR REPLACE FUNCTION union_all_tables()
RETURNS TABLE
(
age bigint
) AS
$$
DECLARE
dynamic_query text = '';
r_row record;
BEGIN
FOR r_row IN SELECT table_schema || '.' || table_name qualified_table_name
FROM information_schema.COLUMNS
WHERE column_name = 'age'
LOOP
dynamic_query := dynamic_query || format('UNION SELECT ' ||
'age ' ||
'FROM %s ',r_row.qualified_table_name) || E'\n'; -- adding new line for pretty print, it is not necessary
END LOOP;
dynamic_query := SUBSTRING(dynamic_query, 7) || ';';
RAISE NOTICE 'Union all tables in staging, executing statement: %', dynamic_query;
RETURN QUERY EXECUTE dynamic_query;
END;
$$
LANGUAGE plpgsql;
You don't need to generate a single huge UNION statement. If you use RETURN QUERY the result of that query is appended to the overall result of the function every time you use it.
When dealing with dynamic SQL you should also use format() to properly deal with identifiers.
Your function can be simplified to:
CREATE OR REPLACE FUNCTION union_all_tables()
RETURNS TABLE (table_schema text, table_name text, age bigint)
AS
$$
DECLARE
dynamic_query text = '';
r_row record;
BEGIN
FOR r_row IN SELECT c.table_schema, c.table_name
FROM information_schema.columns c
WHERE c.column_name = 'age'
LOOP
dynamic_query := format(
'select %L as table_schema, %L as table_name, age from %I.%I',
r_row.table_schema, r_row.table_name,
r_row.table_schema, r_row.table_name);
RETURN QUERY EXECUTE dynamic_query;
END LOOP;
END;
$$
LANGUAGE plpgsql;
Note that the whole function will fail if there is (at least) one table where the age column is not a bigint.

How can I show all tables in data output tab for plpgsql function?

DECLARE
alltables record;
table_all varchar;
BEGIN
for alltables in select distinct table_name , column_name
from information_schema.colunms
loop
table_all = alltables.table_name;
raise notice 'TAB_Name:% , table_all;
end loop;
return table_all;
In here, I can see all tables in (raise notice 'TAB_Name:% , table_all;) message tab in PgAdmin
but Data output tab (return table_all;) return only one column
How can I show all the tables in the data output tab?
I am not sure, if I understand to your query. You wont to write table function probably.
CREATE OR REPLACE FUNCTION xxx
RETURNS TABLE(table_name text, column_name text)
AS $$
BEGIN
FOR table_name, column_name IN
SELECT c.table_name, c.column_name
FROM information_schema.columns
LOOP
RETURN NEXT;
END LOOP;
RETURN;
END;
$$ LANGUAGE plpgsql;
or little bit simply and little bit faster
CREATE OR REPLACE FUNCTION xxx
RETURNS TABLE(table_name text, column_name text)
AS $$
BEGIN
RETURN QUERY
SELECT c.table_name, c.column_name
FROM information_schema.columns
RETURN;
END;
$$ LANGUAGE plpgsql;
you can call it
SELECT * FROM xxx();

Catching an exception while altering a table in Oracle

I'm trying to write a command in Oracle that will wither ADD or MODIFY a column depending on whether or not it already exists. Basically something like:
BEGIN
ALTER TABLE MY_TABLE ADD ( COL_NAME VARCHAR2(100 );
EXCEPTION WHEN OTHERS THEN
ALTER TABLE MY_TABLE MODIFY ( COL_NAME VARCHAR2(100) );
END;
However, Oracle complains about having the ALTER command inside of BEGIN. Is there a way to achieve this using a single SQL command in Oracle?
Thanks!
In order to put DDL in a PL/SQL block, you would need to use dynamic SQL.
Personally, I'd check whether the column exists first and then issue the DDL. Something like
DECLARE
l_cnt INTEGER;
BEGIN
SELECT COUNT(*)
INTO l_cnt
FROM dba_tab_cols
WHERE table_name = 'MY_TABLE'
AND owner = <<owner of table>>
AND column_name = 'COL_NAME';
IF( l_cnt = 0 )
THEN
EXECUTE IMMEDIATE 'ALTER TABLE my_table ADD( col_name VARCHAR2(100) )';
ELSE
EXECUTE IMMEDIATE 'ALTER TABLE my_table MODIFY( col_name VARCHAR2(100) )';
END IF;
END;
If you don't have access to DBA_TAB_COLS, you could also use ALL_TAB_COLS or USER_TAB_COLS depending on what schema the table resides in and what privileges you have on the table.
I found a solution based on this post.
DECLARE v_column_exists number := 0;
BEGIN
SELECT COUNT(*) INTO v_column_exists
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME = 'MY_TABLE'
AND COLUMN_NAME = 'COL_NAME';
IF (v_column_exists = 0) THEN
EXECUTE IMMEDIATE 'ALTER TABLE MY_TABLE ADD ( COL_NAME VARCHAR2(200) )';
ELSE
EXECUTE IMMEDIATE 'ALTER TABLE MY_TABLE MODIFY ( COL_NAME VARCHAR2(200) )';
END IF;
END;