Oracle nullable and non nullable column in a view - sql

I have a table in Oracle. This table has amongst others: EMPLOYEE_ID (NUMBER, Nullable, but number of NULLs is 0) and EMPLOYEE_NUMBER (VARCHAR2, Nullable, but number of NULLs is 0).
Now I create a view on this table:
CREATE OR REPLACE FORCE VIEW APPS.XXKE_L2E_EMPLOYEE
(
PERSON_ID,
EMPLOYEE_NUMBER,
...
)
AS
SELECT EMPLOYEE_ID,
EMPLOYEE_NUMBER,
...
FROM xxke.xxke_employees e
INNER JOIN xxke.xxke_organizations o ON e.organization_id = o.organization_id
INNER JOIN xxke.xxke_operating_units ou ON e.org_id = ou.org_id;
ALTER VIEW APPS.XXKE_L2E_EMPLOYEE
ADD CONSTRAINT XXKE_L2E_EMPLOYEE_V_PK
PRIMARY KEY(PERSON_ID) DISABLE;
How on earth is it possible that in that view PERSON_ID becomes Nullable (even when using NVL trick), but EMPLOYEE_NUMBER is NOT Nullable
QUESTION:
How can I make Oracle to create the view in a way, so the PERSON_ID column becomes NOT Nullable in the view? Or how can I make my WCF oData service query this view. Currently after updating the edmx file I get the following:
Errors: Oracle.ssdl(227,6) : error 0075: Key Part: 'PERSON_ID' for type XXKE_L2E_EMPLOYEE is not valid. All parts of the key must be non nullable.

the reason why it becomes nullable is when creating view query you used nvl on employee_id. The employee is not nullable in the query so that is why it is not. In a view, oracle makes the resulting column nullable. Views are not static but just a query. If you want to not have the issue you can create a materialized view of the table with the nvl pre applied in query then query will not have null possible selected. You can then do indexes and queries will be significantly faster.

If you want your column in the view to be non-nullable, then inner join with the table that contains this column as primary key and select (in this case) PERSON_ID from that table in the view definition. That resolves the problem.

Related

unique constraint on window function output

I want to create lists of items and prevent the entry of 2 identical lists, using a unique constraint on a computed column.
CREATE TABLE test_cc
(
list_id int,
list_item int,
list_items AS STRING_AGG(CONVERT(varchar(10), list_item),',') OVER (PARTITION BY list_id) WITHIN GROUP (ORDER BY list_item),
UNIQUE(bs)
);
INSERT INTO test_cc VALUES (1, 1),(1,2),(2,1),(2,2);
/*should not be possible.*/
Executing this on SQL Server 2019 returns Error Msg 4113 Level 16 during table creation.
Is declaring a unique constraint on an expression a good practice ?
My data volume for this table is not huge.
Making sure that lists are unique are difficult. As I mentioned in the comments you can't use aggregate function in a computed column; a computed column is a value calculated based on the row, not the table.
You also can't use an Indexed View with a UNIQUE INDEX on a STRING_AGG'd column, as STRING_AGG isn't allowed to be used in an indexed view.
One method, therefore, is to use a TRIGGER, however, this won't be performant; in fact as your table grows this is going to get increasingly slower. For a small dataset it should be fine.
CREATE TRIGGER dbo.UnqTrg_list_items_test_cc ON dbo.test_cc
AFTER INSERT,UPDATE,DELETE AS
BEGIN
IF EXISTS (SELECT 1
FROM (SELECT STRING_AGG(cc.list_item,',') WITHIN GROUP (ORDER BY cc.list_item) AS list_items
FROM dbo.test_cc cc
GROUP BY cc.list_id) SA
GROUP BY list_items
HAVING COUNT(list_items) > 1)
THROW 96432, N'Violation of Unique Trigger logic ''UnqTrg_list_items_test_cc''. Cannot insert duplicate list in object ''dbo.test_cc''. The statement has been aborted.',10;
END;
db<>fiddle demonstrating INSERT,DELETE and UPDATE failing.

Inserting from one table or view into another but avoid combination duplicates- Can this be done in SQL? If so how?

I have a table that has a primary key WORKITEMID, and the following 3 foreign keys PRODSERVID,PROCESSID,and TASKKNOWID.
I have a view that I can create that also has PRODSERVID,PROCESSID, AND TASKKNOWID. This view will usually have ALL the records in above table plus some new ones - not in the table. The 'table' by definition is meant to hold the unique combinations of PRODSERVID, PROCESSID, and TASKKNOWID.
I would like to insert from the view into the table any new combinations in the view not present in the table. And I don't want to overwrite the existing WORKITEMIDs in the INSERT- because those WORKITEMIDs are used elsewhere.
Can this be done in SQL?
Thanks
Absolutely, the simplest form of criteria for this is to use the negation of EXISTS()
INSERT INTO [TableName] (PRODSERVID,PROCESSID,TASKKNOWID,... )
SELECT PRODSERVID,PROCESSID,TASKKNOWID,...
FROM [ViewName] v
WHERE NOT EXISTS (
SELECT 1 FROM [TableName] t
WHERE t.PRODSERVID = v.PRODSERVID AND t.PROCESSID = v.PROCESSID AND t.TASKKNOWID = v.TASKKNOWID
)
replace the ... with your other fields
You could also use a non-corellating outer join but I find not exists makes the intent much clearer.
There is a good comparison of the different approaches to this issue in this article: T-SQL commands performance comparison – NOT IN vs SQL NOT EXISTS vs SQL LEFT JOIN vs SQL EXCEPT

Most elegant way to create 'subrecord' type keeping names of columns

So I'm playing with Postgres' composite types, and I cannot figure out one thing. Suppose I want to use subset of columns of certain table, or mix of different columns of several different tables used in the query, and create a record type out of them.
Logically, simple (c.id, c.name) should work, but it seems that column names are actually lost - it's not possible to address fields of the records by name and id, and, for example, to_json function cannot use field names when creating json out of this record. Using subquery (select c.id, c.name) is predictably failing with subquery must return only one column error.
I can, of course, use lateral join or common table expression to create this sub-type, but I'm thinking - if there's more elegant way?
see db<>fiddle demo with table example and test query
create table test(id integer, name text, price int);
insert into test(id,name,price)
values
(1,'name1',1),
(2,'name2',12),
(3,'name3',23),
(5,'name5',4),
(9,'name9',3);
create type sub_test as (id integer, name text);
select
c.price,
-- using predefined type - works
to_json((c.id, c.name)::sub_test),
-- creating row type on the fly - doesn't work, names are lost
to_json((c.id, c.name)),
-- using derived table created with lateral join - works
to_json(d)
from test as c, lateral(select c.id, c.name) as d

How to enforce key uniqueness in a temporal table?

What is the best way to enforce key uniqueness in a temporal table (Oracle DBMS). A temporal table is one where all historical states are recorded with a time-span.
For example, we have a Key --> Value association like this ...
create table TEMPORAL_VALUES
(KEY1 varchar2(99) not null,
VALUE1 varchar2(99),
START_PERIOD date not null,
END_PERIOD date not null);
There are two constraints to enforce to do with the temporal nature of the table, to wit:
For each record we must have END_PERIOD > START_PERIOD. This is the period for which the Key->Value map is valid.
For each Key, there can't be any overlapping periods. The period includes the moment of the START_PERIOD, but excludes the exact moment of the END_PERIOD.
Constraint enforcement could be done either on row insert/update, or on commit. I don't really care, as long as it is impossible to commit invalid data.
I've been informed that the best practice to enforce constraints like this is to use materialized views instead of triggers.
Please advise on what is the best way to achieve this?
The Oracle banner is ...
Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - 64bi
What I have tried so far
I think that this solution is close, but it doesn't really work because 'on commit' is needed. Oracle doesn't seem capable of creating a materialized view of this complexity which refreshes on commit.
create materialized view OVERLAPPING_VALUES
nologging cache build immediate
refresh complete on demand
as select 'Wrong!'
from
(
select KEY1, END_PERIOD,
lead( START_PERIOD, 1) over (partition by KEY1 order by START_PERIOD) as NEXT_START
from TEMPORAL_VALUES
)
where NEXT_START < END_PERIOD;
alter table OVERLAPPING_VALUES add CHECK( 0 = 1 );
What am I doing wrong? How do I get this work on commit to prevent invalid rows in TEMPORAL_VALUES?
After some struggling, experimentation and guidance from this forum post,
drop table TEMPORAL_VALUE;
create table TEMPORAL_VALUE
(KEY1 varchar2(99) not null,
VALUE1 varchar2(99),
START_PERIOD date not null,
END_PERIOD date
)
/
alter table TEMPORAL_VALUE add
constraint CHECK_PERIOD check ( END_PERIOD is null or END_PERIOD > START_PERIOD)
/
alter table TEMPORAL_VALUE add
constraint PK_TEMPORAL_VALUE primary key (KEY1, START_PERIOD)
/
alter table TEMPORAL_VALUE add
constraint UNIQUE_END_PERIOD unique (KEY1, END_PERIOD)
/
create materialized view log on TEMPORAL_VALUE with rowid;
drop materialized view OVERLAPPING_VALUES;
create materialized view OVERLAPPING_VALUES
build immediate refresh fast on commit as
select a.rowid a_rowid, b.rowid b_rowid
from TEMPORAL_VALUE a, TEMPORAL_VALUE b
where a.KEY1 = b.KEY1
and a.rowid <> b.rowid
and a.START_PERIOD <= b.START_PERIOD
and (a.END_PERIOD is null or (a.END_PERIOD > b.START_PERIOD));
alter table OVERLAPPING_VALUES add CHECK( 0 = 1 );
Why does this work?
Why does this work, but my original posted view ...
select KEY1, END_PERIOD,
lead( START_PERIOD, 1) over (partition by KEY1 order by START_PERIOD) as NEXT_START
from TEMPORAL_VALUES
... will not be accepted as an On-Commit materialized view? Well, there answer is that there appears to be limits in the complexity of on-commit materialized views. The views must include the row id's or keys of the underlying table, and not be over some threshold of complexity.
There is a technique I've seen described for SQL Server (see this article and search for "Kuznetsov's History Table") which adds a third time column, previous_end_period that you can use to establish a foreign key on the table itself to enforce the constraint that the intervals can't overlap. I don't know if this can be adapted to Oracle.
Nice solution Sean!
But I would add comments to your objects due to the complexity… something like:
COMMENT ON COLUMN TEMPORAL_VALUE.KEY IS 'Each key may have at most only one value for any instant in time';
COMMENT ON COLUMN TEMPORAL_VALUE.START_PERIOD IS 'The period described includes the START_PERIOD date/time';
COMMENT ON COLUMN TEMPORAL_VALUE.END_PERIOD IS 'The period described does not included the END_PERIOD date/time. A null end period means until forever';
COMMENT ON COLUMN TEMPORAL_VALUE IS 'Integrity is enforced by the MATERIALIZED VIEW OVERLAPPING_VALUES';
COMMENT ON MATERIALIZED VIEW OVERLAPPING_VALUES IS 'Used to enforce the rule - each key may have at most only one value for any instant in time. This is an [on commit] mv, that holds any temporal values that overlaps another (for the same key), but the CHECK(0=1) constraint will raise an exception if any rows are found, stopping any commit that would break integrity';
I personally like to prefix all materialized view names with MV_ and views with V_
Interesting that you don’t allow START_PERIOD to be null. Most implementations would allow a null start and a non-null end to specify the period everything before, and null values for both bates to indicate a constant value for a key.

Create a unique primary key (hash) from database columns

I have this table which doesn't have a primary key.
I'm going to insert some records in a new table to analyze them and I'm thinking in creating a new primary key with the values from all the available columns.
If this were a programming language like Java I would:
int hash = column1 * 31 + column2 * 31 + column3*31
Or something like that. But this is SQL.
How can I create a primary key from the values of the available columns? It won't work for me to simply mark all the columns as PK, for what I need to do is to compare them with data from other DB table.
My table has 3 numbers and a date.
EDIT What my problem is
I think a bit more of background is needed. I'm sorry for not providing it before.
I have a database ( dm ) that is being updated everyday from another db ( original source ) . It has records form the past two years.
Last month ( july ) the update process got broken and for a month there was no data being updated into the dm.
I manually create a table with the same structure in my Oracle XE, and I copy the records from the original source into my db ( myxe ) I copied only records from July to create a report needed by the end of the month.
Finally on aug 8 the update process got fixed and the records which have been waiting to be migrated by this automatic process got copied into the database ( from originalsource to dm ).
This process does clean up from the original source the data once it is copied ( into dm ).
Everything look fine, but we have just realize that an amount of the records got lost ( about 25% of july )
So, what I want to do is to use my backup ( myxe ) and insert into the database ( dm ) all those records missing.
The problem here are:
They don't have a well defined PK.
They are in separate databases.
So I thought that If I could create a unique pk from both tables which gave the same number I could tell which were missing and insert them.
EDIT 2
So I did the following in my local environment:
select a.* from the_table#PRODUCTION a , the_table b where
a.idle = b.idle and
a.activity = b.activity and
a.finishdate = b.finishdate
Which returns all the rows that are present in both databases ( the .. union? ) I've got 2,000 records.
What I'm going to do next, is delete them all from the target db and then just insert them all s from my db into the target table
I hope I don't get in something worst : - S : -S
The danger of creating a hash value by combining the 3 numbers and the date is that it might not be unique and hence cannot be used safely as a primary key.
Instead I'd recommend using an autoincrementing ID for your primary key.
Just create a surrogate key:
ALTER TABLE mytable ADD pk_col INT
UPDATE mytable
SET pk_col = rownum
ALTER TABLE mytable MODIFY pk_col INT NOT NULL
ALTER TABLE mytable ADD CONSTRAINT pk_mytable_pk_col PRIMARY KEY (pk_col)
or this:
ALTER TABLE mytable ADD pk_col RAW(16)
UPDATE mytable
SET pk_col = SYS_GUID()
ALTER TABLE mytable MODIFY pk_col RAW(16) NOT NULL
ALTER TABLE mytable ADD CONSTRAINT pk_mytable_pk_col PRIMARY KEY (pk_col)
The latter uses GUID's which are unique across databases, but consume more spaces and are much slower to generate (your INSERT's will be slow)
Update:
If you need to create same PRIMARY KEYs on two tables with identical data, use this:
MERGE
INTO mytable v
USING (
SELECT rowid AS rid, rownum AS rn
FROM mytable
ORDER BY
co1l, col2, col3
)
ON (v.rowid = rid)
WHEN MATCHED THEN
UPDATE
SET pk_col = rn
Note that tables should be identical up to a single row (i. e. have same number of rows with same data in them).
Update 2:
For your very problem, you don't need a PK at all.
If you just want to select the records missing in dm, use this one (on dm side)
SELECT *
FROM mytable#myxe
MINUS
SELECT *
FROM mytable
This will return all records that exist in mytable#myxe but not in mytable#dm
Note that it will shrink all duplicates if any.
Assuming that you have ensured uniqueness...you can do almost the same thing in SQL. The only problem will be the conversion of the date to a numeric value so that you can hash it.
Select Table2.SomeFields
FROM Table1 LEFT OUTER JOIN Table2 ON
(Table1.col1 * 31) + (Table1.col2 * 31) + (Table1.col3 * 31) +
((DatePart(year,Table1.date) + DatePart(month,Table1.date) + DatePart(day,Table1.date) )* 31) = Table2.hashedPk
The above query would work for SQL Server, the only difference for Oracle would be in terms of how you handle the date conversion. Moreover, there are other functions for converting dates in SQL Server as well, so this is by no means the only solution.
And, you can combine this with Quassnoi's SET statement to populate the new field as well. Just use the left side of the Join condition logic for the value.
If you're loading your new table with values from the old table, and you then need to join the two tables, you can only "properly" do this if you can uniquely identify each row in the original table. Quassnoi's solution will allow you to do this, IF you can first alter the old table by adding a new column.
If you cannot alter the original table, generating some form of hash code based on the columns of the old table would work -- but, again, only if the hash codes uniquely identify each row. (Oracle has checksum functions, right? If so, use them.)
If hash code uniqueness cannot be guaranteed, you may have to settle for a primary key composed of as many columns are required to ensure uniqueness (e.g. the natural key). If there is no natural key, well, I heard once that Oracle provides a rownum for each row of data, could you use that?