Friday, October 3, 2008

It Seems You Can't Trust Anybody These Days

Here's the deal: I'm planning to take the beta advanced PL/SQL exam. To help me study, I purchased the Oracle Press 11g PL/SQL Programming book by Michael McLaughlin. I was quite impressed with the number of topics covered (especially in the appendices).

As I could not find this book in any bookstore, I had to order it semi-blind from Amazon. And I was not disappointed in the number or depth of topics covered. The style is similar to that of an old time reference manual. Think, the man page for ksh. But, I can live with that. I have learned a lot from reading the man page for ksh. And bash. And sh. But not csh. I have to draw the line somewhere. But, I digress.

Things were going fine until I came to the part where the author stated that all Oracle professionals must understand how to use triggers to enforce complex R.I. Since Tom Kyte's writings had convinced me that this was probably not going to happen, I took special notice.

First, a bit about his example. He had two tables:


MEMBER
Name Null? Type
----------------------- ------- -----
MEMBER_ID NOT NULL NUMBER
MEMBER_TYPE NOT NULL NUMBER
ACCOUNT_NUMBER NOT NULL VARCHAR2(10)
CREDIT_CARD_NUMBER NOT NULL VARCHAR2(19)
CREDIT_CARD_TYPE NOT NULL NUMBER
CREATED_BY NOT NULL NUMBER
CREATION_DATE NOT NULL DATE
LAST_UPDATED_BY NOT NULL NUMBER
LAST_UPDATE_DATE NOT NULL DATE


and CONTACT:

Name Null? Type
----------------------- ------- -----
CONTACT_ID NOT NULL NUMBER
MEMBER_ID NOT NULL NUMBER
CONTACT_TYPE NOT NULL NUMBER
LAST_NAME NOT NULL VARCHAR2(20)
FIRST_NAME NOT NULL VARCHAR2(20)
MIDDLE_INITIAL VARCHAR2(1)
CREATED_BY NOT NULL NUMBER
CREATION_DATE NOT NULL DATE
LAST_UPDATED_BY NOT NULL NUMBER
LAST_UPDATE_DATE NOT NULL DATE


Where contact_id is the primary key of the CONTACT table, member_id is the primary key of the MEMBER table; contact.member_id refers back to member.member_id, and the constraint to be implemented is that there can be one or two contacts for a given member in the CONTACT table, but never more than two. And he offers up this trigger as a way to enforce this business rule:


CREATE OR REPLACE TRIGGER contact_t1
BEFORE INSERT ON contact
FOR EACH ROW
DECLARE
CURSOR c ( member_id_in NUMBER ) IS
SELECT null
FROM contact c
, member m
WHERE c.member_id = m.member_id
AND c.member_id = member_id_in
HAVING COUNT(*) > 1;
BEGIN
FOR i IN c (:new.member_id) LOOP
RAISE_APPLICATION_ERROR(-20001,'Already two signers.');
END LOOP;
END;
/

There are a few interesting points about the code. First of all, I was left wondering why this did not cause a mutating table error. A few minutes of searching Ask Tom revealed that a single row insert does not raise the error; Oracle is able to deduce the complete state of the table in the midst of the change.

The other interesting thing is the design of the trigger. All it does is try to open a cursor and fetch a row. If it is able to do so, then there already are two rows in the table, and the triggering statement is attempting to insert a third. So it doesn't bother to do anything with the row it implicitly fetched; if it can open the cursor an error has occurred.

So, to test things out, we add a new member, ID=1004, to the member table, and then start adding contacts to the contact table:


SQL> insert into contact values
2 (1010, 1004, 1003, 'Dodah', 'Day', ' ', 2,sysdate, 2, sysdate);

1 row created.

SQL> commit;

Commit complete.

SQL> insert into contact values
2 (1011, 1004, 1003, 'Dodah', 'Day', ' ', 2,sysdate, 2, sysdate);

1 row created.

SQL> insert into contact values
2 (1012, 1004, 1003, 'Dodah', 'Day', ' ', 2,sysdate, 2, sysdate);
insert into contact values
*
ERROR at line 1:
ORA-20001: Already two signers.
ORA-06512: at "SCOTT.CONTACT_T1", line 11
ORA-04088: error during execution of trigger 'SCOTT.CONTACT_T1'

SQL>


which shows that the trigger works fine in a single user situation. So, we rollback our 2nd insert and repeat it:


SQL> insert into contact values
2 (1011, 1004, 1003, 'Dodah', 'Day', ' ', 2,sysdate, 2, sysdate);

1 row created.
And we start up a 2nd session and try

SQL> insert into contact values
2 (1012, 1004, 1003, 'Dodah', 'Day', ' ', 2,sysdate, 2, sysdate);

1 row created.
And we start up a 3rd session and try

SQL> insert into contact values
2 (1013, 1004, 1003, 'Dodah', 'Day', ' ', 2,sysdate, 2, sysdate);

1 row created.

SQL> Commit;

Commit complete.

SQL>
And we then commit in sessions 2 and 1. When we are all done, we have not three, but four signers for this member.

The problem, of course is that when sessions 2 and 3 tried to insert their row, the trigger was unable to "see" the uncommitted row in session 1. Therefore, the trigger quite properly allowed us to continue with the insert.

Now the question is, what is the best way (if any) to enforce this business rule? One thing we could try is to add a materialized view:

create materialized view member_ck
build immediate
refresh on commit
enable query rewrite
as
select c.member_id, count(*) kount
from contact c, member m
where c.member_id = m.member_id
group by c.member_id
/

alter table member_ck add constraint ck_kount check (kount <= 2);

If we do this, we find that we can repeat our opening situation, however, as soon as a commit would leave three or more members in the table we get

SQL> commit;
commit
*
ERROR at line 1:
ORA-12008: error in materialized view refresh path
ORA-02290: check constraint (SCOTT.CK_KOUNT) violated

Which will work, but is a little awkward, as the constraint is enforced at the transaction level rather than the statement level. Another approach is to add a LOCK TABLE statement to the trigger:

CREATE OR REPLACE TRIGGER contact_t1
BEFORE INSERT ON contact
FOR EACH ROW
DECLARE
CURSOR c ( member_id_in NUMBER ) IS
SELECT null
FROM contact c
, member m
WHERE c.member_id = m.member_id
AND c.member_id = member_id_in
HAVING COUNT(*) > 1;
BEGIN
LOCK TABLE contact IN EXCLUSIVE MODE;
FOR i IN c (:new.member_id) LOOP
RAISE_APPLICATION_ERROR(-20001,'Already two signers.');
END LOOP;
END;
/

This accomplishes all the R.I. requirements, at the expense of destroying concurrency. When a second session attempts to insert (or update, for that matter) a row, it hangs until the first session has committed or rolled back.

To return to the beginning of this post, it really is important to know how to enforce complex R.I. And I don't have any more answers than the next scribe when it comes to doing so. But I can spot a proposed answer which does not work.

Sunday, September 7, 2008

(Almost) Everything I Know About Character Sets

Here is another issue which recently came up at work. Since I seem to get asked these questions about every year or so, I think I will write them up once and for all. To make this fit into my computer's memory, I'm going to be very imprecise, and use the term "character set" in places where I really should use "code point" or "encoding" or "code page". And I am never going to use the term "glyph". If you already understand the above, you probably do not need to read the rest.

But first, a little fable. Mr. A is about to insert a row of text into an Oracle database. Exactly what that text consists of is not important, other than the 42nd character of the text is the euro symbol.

And I'm not going to try to write this symbol on this page, as the chances that the reader will not be able to see it are good. Just keep in mind that Mr. A has attempted to insert a euro character into an Oracle table


Now, Mr. A is doing his work running SQL*Plus on Linux. He is using the ISO8859-15 character set. This is a modification of the original ISO8859-1 character set, contains 256 "characters", and is a superset of the old US ASCII 7 bit character set. It is sometimes referred to (incorrectly) as "8 bit ASCII". If we were to do a memory probe of his session, we would find that the 42nd character of the text is coded as A4 hex (164 decimal).

The database into which this text gets inserted is using the UTF8 character set. In this character set, every character is coded using from 1 to 6 bytes. And it happens that the euro is supposed to be represented by the bytes "20 AC". In UTF8, an A4 as a single byte character represents the universal default currency symbol.

No sooner is the row committed, than Ms B decides to fetch it. Her Windows system uses the Win-1252 character set, where the euro is represented by a hex 80 (128 decimal). Yet, she still manages to view the euro symbol correctly.

Clearly, there is a whole lot of conversion going on.

The rest of this discussion is best delivered in a Q & A format, beginning with Question # 1:

Q1: So, how does the database manage all those conversions?

A: The database does not manage any conversions; they are all performed by the clients. Mr A's and Ms. B's client software (sql*plus, Toad, Sql Developer, some home grown application) all know what character set their system is using and what character set the database is using, and it converts accordingly.


Q2:
The Oracle Client software must be pretty smart. After reading your fable, I've been inspired to try to figure out what character set I am using, and I realize that I don't have the foggiest notion of where to start. Yet, Oracle is able to do this on the fly. Any hints for me?

A: Don't feel so bad; Oracle doesn't know what you are actually using. It just knows what you told it you were using.

Q3: You've outdone yourself in obscurity. Can you please expand on that last point?

A: The simple truth is, there is no way to know what characters a string of bytes means unless you also know the character set that string has been encoded with. In order for Oracle to understand what the text, you must give it this information. This is done via the NLS_LANG variable. You set the NLS_LANG environment variable, and this tells the client software what character set you are using. If you fail to set the NLS_LANG variable, the client will default to 7 bit US ASCII (more precisely, AMERICAN_AMERICA.US7ASCII). The only way to avoid character conversion problems is to (a) know what character set your system is using and (b) set an appropriate NLS_LANG value.

Q4: Why can't I just ask my DBA what character set the database is using, and use that for my NLS_LANG?

A: Because your DBA might just play a trick on you and give you an honest answer. Then, you are in all sorts of trouble. Because you didn't follow the answer to Q3.

Actually, there is a more complete answer to this question. As you should have deduced by now, the Oracle client has to do a lot of work when it has to do character set conversions. So, Oracle introduced a little optimization: If the client sees that the database value of NLS_LANG is the same as it's NLS_LANG, no conversion takes place. Now, before you run off and think that all your issues have been resolved, consider the implications of a character set mismatch.

Same situation as my Fable, except that Mr. A has set NLS_LANG to the same value as the database. Now when he inserts his text, the 42nd character does not get inserted as "20 AC" but as "A4". And if he comes back tomorrow and selects it, it will come back as a euro symbol. That is because, on his personal system, "A4" codes the euro symbol.

But, what happens to poor Ms. B? Her client is going to fetch the same "A4" from the database, but it is going to run through the conversion routine, realize that in UTF8, "A4" encodes a generalized currency symbol, convert it to the correct code for Ms. B (unfortunately, it also is A4; I wasn't able to get every possible twist and turn here) and present the wrong character to her.

Q5: If Windows can figure out what character set I am using just by looking at my file, why can't Oracle?

A: Because Bush hid the truth?

Seriously.

There is a well documented bug (do a search on "Bush hid the truth") in Windows 2000/XP which exposes what happens when Windows tries to do exactly that. You can see this for yourself. Create a .txt document in Notepad, containing just the one line "Bush hid the truth" (there are other lines which also work, but this one is the most famous). Save it and reopen it. You will find yourself looking at 9 Chinese characters. And why this happens is instructive.

As I said before, the plain fact is, given a what is supposed to be a plain text file, one has no idea how to interpret it unless one is told what character set that file is using. And there is no place to store this metadata in a Windows .txt file (or in *nix for that matter). So, when Windows tries to open up that file, it must guess at it's character set. In the case of a text file containing just the windows 1252 characters for "Bush hid the truth", it guesses wrong.

Q6: I've got a web server, an application server, a database server, users in the USA, Canada, Germany, what am I to do?

A: And you thought Jack Bauer had a tough life.

O.K., here's the way things are supposed to work, with some comments on where things can go wrong.

First of all, nobody just enters data into a web page; one really enters data into a web form. To get that web form displayed in your web browser, you must request it from a web server. And that web server, if it is configured correctly, is going to untangle this web I have woven by doing two things:

a) It is going to send a Content-Type header containing the character set name to your browser before it sends your page.

b) That page is going to contain a similar header as a meta tag right at the beginning, just in case the web server was not configured correctly.

Armed with this bit of Inside Information, your browser knows how to interpret the text on the web page. And it knows what character set to use when it returns the form after you have filled it out.

Now, there are two things which can go wrong here. First, the webmaster may have failed to implement points (a) and (b) above, in which case your browser has no idea what character set it should use. In this case, the results are browser dependent. With Firefox, you set a default character set, which is used if the web page does not specify one. Internet Explorer tries to deduce which set to use. Usually it gets it right (except when Bush hides the truth).

The second thing which can go wrong is that the user can decide to enter text using a different character set than the web page is expecting. Suppose the web page headers specify 8859-1. Suppose the user is cutting and pasting from some European character set. What gets sent back to the web server are the byte strings for those European characters, which have different meanings in the 8859-1 character set. So user thinks he has entered a euro, and the web server thinks he has entered a currency sign.

Meanwhile, back at the data center, the application is trying to insert this data in an Oracle database. It should be clear by now that the application should have set NLS_LANG to be consistent with whatever character set the web server is trying to use. If it is, then the data should be entered into the database consistently. Or, at least, if there are any character conversion errors, they can be blamed on the user and the webmaster.

Once the data gets entered correctly, viewing it is a simple matter of getting NLS_LANG set up correctly on your client software. And, in this case, the client is the web server (which was set up correctly in the last paragraph). If you are having a problem viewing what your users are viewing when you use Toad, you need to see if you are using the same NLS_LANG that your users are using.

For further reading, I recommend starting with these three sources:

Character sets in general: the Joel on Software article on character sets

Character sets and the web: this Sitepoint article

How Oracle handles them: The Oracle Globalization manual for your database version.

Wednesday, March 19, 2008

ORA-0000 (SYSDATE Traps)

Quick confession: I never took the 9i TIMESTAMP datatype very seriously. I looked at it merely as a way to record fractional parts of a second. I never needed to record fractional parts of a second.

Then came the Great Daylight Savings Time Scare of 2007. I was glad I never paid attention to TIMESTAMP datatypes. If I had, I might have had a problem. No time zone aware columns, no problem.

Until I wrote a task which was used by all sites in my application to drive cron.

Suddenly, a number of sites starting running tasks repeatedly, starting an hour early. Mysteriously, they all were in the Central time zone.

Here is what the problem was. Cron was using Central time. Even though it was in a data center in the Eastern time zone, the VM server had America/Chicago for its TZ variable. But SYSDATE was returning Eastern time, because it too was in the Eastern time zone, and the listener and spawned dedicated Sql*net servers were all Eastern time. Since my task derived the time from SYSDATE, it was an hour off.

The fix was to replace SYSDATE with CURRENT_DATE. CURRENT_DATE returns the database server time adjusted for the client's time zone. SYSDATE is never adjusted.

A few other functions I should have learned earlier:

DBTIMEZONE: Returns the offset from Zulu time of the database.

SESSIONTIMEZONE: As above, but for the session.

SYSTEMTIMESTAMP: Like SYSDATE, but returns a timestamp with time zone offset

CURRENT_TIMESTAMP: This timestamp is adjusted for the session.

LOCALTIMESTAMP: Like CURRENT_TIMESTAMP, but without the time stamp offset.

Saturday, March 8, 2008

PL/SQL Version of DAYS360

While curiously checking where I had contributed to AskTom, I happened to note that someone had responded (a good year after the last post) to a question asking for a PL/SQL function to emulate the Excel days360 function. This brought back so many memories of when I worked in the credit insurance area that I immediately had to write one. For there are many complications in day counting, more than the novice would suspect. Here I am going to repeat the listing, but also discuss a number of issues for which there was not room on the AskTom site. And, before I say anything else, let me point out that this is going to be a very U.S. consumer loan oriented discussion.


Let me state up front that I will only be discussing loans with no compounding of interest, more precisely, where interest is not earned on interest. In practice, this is how most of the consumer loans in the U.S. work. With this assumption, the interest (I) due after one payment interval (T) is the product of the interest rate (R) and the loan balance (A):



I = A * R


For example, if one has a loan of $10,000, and an annual interest rate of 12%, the interest due at the end of the first year is (10000) * (0.12) = $1200.


The interest rate is usually stated as the nominal annual interest rate, since it refers to the interest rate for a period of one year. For those few loans with annual payments (T = one year), this is all one needs. If the payment interval is something other than a year, then one needs to derive the effective interest rate (i) from the nominal rate.


For mortgage and many car loans, the payment interval is one month. Calculating i is then a simple matter of dividing I by 12. To continue our example, if the payment interval were the month instead of the year, the interest due at the end of the first month would be (10000) * (0.12 / 12) = $100.


For credit cards and money market deposits, however, the unit of time is the day, and i is often (but not always!) calculated by dividing I by 365. Why 365? Because there are supposed to be 365 days in a year. And yes, even in 2008, if you check the fine print in your credit card statement, you will probably see that the daily rate is 1/365th of the annual rate.


Oops. We no longer have the payment period equal to the interest interval. If (D) is the number of days between payments, our formula becomes



I = A * D * (R / 365)


Or, if we go 30 days in our example, I = (10000) * 30 * (0.12 / 365) = $98.63


Of course, if we went 31 days instead, I = (10000) * 31 * (0.12 / 365) = $101.92


Double oops. It looks as if the payee in the above exaple wants to make a monthly payment. Wouldn't it be nice if we could get the interest to be equal to our monthly example of $100? We could if we let the days in a month be 30 (always and everywhere) and changed the 365 to 360.


We finally can describe the wherefore and why of the days360 function. Days360 returns the number of days between two dates, and it tries to do it as if each year had exactly 360 days in it, and each month had exactly 30 days. Of course, eight months do not have 30 days, and there are several reasonable ways to treat those months. The European method (which is invoked by setting the third parameter to 'true') changes the last day of the month to the 30th of the month. The U.S. method (which is invoked by setting the third parameter to 'false') is more complicated, and the Excel help screen should be read if you really want all the details.


Anyway, here is a PL/SQL version of 'days360':


create or replace function days360(
p_start_date date,
p_end_date date,
p_rule_type char default 'F'
)
RETURN number
IS
v_mm1 pls_integer;
v_dd1 pls_integer;
v_yyyy1 pls_integer;
v_mm2 pls_integer;
v_dd2 pls_integer;
v_yyyy2 pls_integer;
BEGIN
v_yyyy1 := to_number(to_char(p_start_date,'yyyy'));
v_mm1 := to_number(to_char(p_start_date,'mm'));
v_dd1 := to_number(to_char(p_start_date,'dd'));
v_yyyy2 := to_number(to_char(p_end_date,'yyyy'));
v_mm2 := to_number(to_char(p_end_date,'mm'));
v_dd2 := to_number(to_char(p_end_date,'dd'));
IF p_rule_type = 'F' THEN
IF v_dd1 = 31 THEN v_dd1 := 30; END IF;
IF v_mm1 = 2 AND v_dd1 = to_number(to_char(last_day(p_start_date),'dd'))
THEN v_dd1 := 30; END IF;
IF v_dd2 = 31
THEN IF v_dd1 < 30
THEN v_dd2 := 1;
v_mm2 := v_mm2 + 1;
IF v_mm2 = 13 THEN v_mm2 := 1;
v_yyyy2 := v_yyyy2 +1;
END IF;
ELSE v_dd2 := 30;
END IF;
END IF;
IF v_mm2 = 2 AND v_dd2 = to_number(to_char(last_day(p_end_date),'dd'))
THEN v_dd2 := 30;
IF (v_dd1 < 30)
THEN v_dd2 := 1;
v_mm2 := 3;
END IF;
END IF;
IF v_mm2 IN (4, 6, 9, 11) AND v_dd2 = 30
AND v_dd1 < 30
THEN v_dd2 := 1;
v_mm2 := v_mm2 + 1;
END IF;
ELSIF p_rule_type = 'T' THEN
IF v_dd1 = 31 THEN v_dd1 := 30; END IF;
IF v_dd1 = 31 THEN v_dd1 := 30; END IF;
IF v_mm1 = 2 AND v_dd1 = to_number(to_char(last_day(p_start_date),'dd'))
THEN v_dd1 := 30; END IF;
IF v_dd2 = 31 THEN v_dd2 := 30; END IF;
IF v_mm2 = 2 AND v_dd2 = to_number(to_char(last_day(p_end_date),'dd'))
THEN v_dd2 := 30; END IF;
ELSE RAISE_APPLICATION_ERROR('-20002','3VL Not Allowed Here');
END IF;
RETURN (v_yyyy2 - v_yyyy1) * 360
+ (v_mm2 - v_mm1) * 30
+ (v_dd2 - v_dd1);
END;

/


In testing this, I set my NLS_DATE_FORMAT to YYYYMMDD, and I will use that format in this discussion.


Note that days360('20060730', '20070730','T') = days360('20060730', '20070730','T') = 360, as one would expect for a 360 day year. And the same holds true if you change either of the dates to the 31st of the month.

[And the reason I am not using any dates in 2008 is that I want to avoid the complication of leap year]

Now, suppose someone borrows some money on 20060731, and pays the loan back with payments on the last day of each month. In other words, the payment intervals are



Start Date Pay Date
---------- ---------
20060731 20060831
20060831 20060930
20060930 20061031
20061031 20061130
20061130 20061231
20061231 20070131
20070131 20070228
20070228 20070331
20070331 20070430
20070430 20070531
20070531 20070630
20070630 20070731


If you do the days360 calculation on those payment intervals, you will find that each interval has 30 days, and the 12 intervals in the year add up to 360 days. It seems that the days360 function is doing its job.


Finally, let's assume that someone else takes out an identical loan, but that this person makes some payments early. No payments are later than the last of the month, but some are made earlier. Let's see what happens:


Start Pay European U.S.
Date Date Days Days
------- --------- --------- -------
20060731 20060830 30 30
20060830 20060929 29 29
20060929 20061030 31 32
20061030 20061129 29 29
20061129 20061230 31 32
20061230 20070128 28 28
20070128 20070227 29 29
20070227 20070331 33 34
20070331 20070429 29 29
20070429 20070531 31 32
20070531 20070629 29 29
20070629 20070731 31 32
==== ===
Totals 360 365

That's right, you get dinged an extra 5 days interest from trying to pay your loan off early.


Now do you believe there are complications in day counting?

Saturday, January 19, 2008

ORA 1422, 1427, 2112 (query returns two many rows)

Three error codes, which all mean the same thing: a query which syntactically must return a single row returns multiple rows.

In a 3GL program, a SELECT ... INTO ... must return just one row. If a PL/SQL routine returns multiple rows, a 1422 is returned. The 2112 is returned from a precompilier program. The 1427 is returned when a subquery must return one row, and it does not. For example:

SELECT ....
WHERE column_name = (some subquery).

This error is easy to verify: just run the query. If it returns more than one row there is nothing more to investigate. One situation where it may be tricky to identfy is a PL/SQL program with this construct:

SELECT ... INTO ... FROM ...
WHERE column_name = variable_name

and the column_name and the variable_name are identical. For example:

DECLARE
this_column number;
BEGIN
SELECT COUNT(*) INTO KOUNT FROM SOME_TABLE
WHERE this_column = this_column;

This is ambiguous: does the right hand side represent SOME_TABLE.THIS_COLUMN,
or the variable? PL/SQL takes it as the column name, and all rows get returned.

The 1427 can also arise in an UPDATE statement:

UPDATE ....
SET column_name = (some subquery)

If the subquery returns two or more rows, a 1427 results.

What to do:

Knowing what is wrong is easy; fixing is another matter. In theory, you have misstated your query; just add additional predicates to make it return a single row. Problems arise when there is bad data (the query should return just one row, but it doesn't) or when you expect multiple rows, but really don't care. The first case is simple: just delete the duplicate data and add enough constraints to the application so that it can't happen again. Where the designer decided to have the application enforce constraints (because it is too much work for the poor database to enforce uniqueness) this may be difficult, and things may devolve into the second case.

In the second case, you really can get multiple rows back; you just thought (possibly because of incomplete test cases) that you would just get one row back. As an example, you are given an e-mail address, and need to decide whether to go to the new account screen, or bring up an existing account. Normally, there is just one account, but a few people may have multiple accounts. If you need to know every account you have no choice but to replace the singleton select with a cursor. But, you may only need to know if there are multiple accounts; you do one thing if there are no accounts, and something else if there is one or more accounts, and you do the same thing regardless of the value of the accounts.

The easiest trick is the old SELECT count (*). If this can be satisfied by an index, and you don't care what the account value is, then this is probably the best approach. If you have to do a full table scan, this is less desirable. If you do need the value, then you will have to do a second query to get the value, and this simply does not work.

The best approach, if you need a value from a query, and you don't care what the value is, is to add

AND ROWNUM = 1

to your WHERE clause. If there are multiple values, this returns the first one found (and does it as quickly as possible). If there is one value, you get it. And, if there are no values, it raises the NO_DATA_FOUND exception, which is what your code was already expecting (you were prepared for this exception, were you not?) In particular, this avoids any sort which would be produced if the DISTINCT operator were used instead, and it works if there are different values present.

One trick which is more trouble than it is worth to use the MAX or MIN function to make all values
the same. This may introduce a sort, and will always return a row, so you will have to modify your
error checking.

One Interesting Table!

I have some thoughts on a number of topics, and this table will serve as a good example for all of them. Let's first start with some DDL:

CREATE TABLE PROSPECTS (
PROSPECT_ID NUMBER PRIMARY KEY,
FIRST_NAME VARCHAR2(25),
LAST_NAME VARCHAR2(25),
SSN VARCHAR2(9),
STREET VARCHAR2(30),
CITY VARCHAR2(30),
STATE_CD VARCHAR2(2),
PHONE VARCHAR2(10)
)
/

And let's fill it with some data:


PROSPECT FIRST LAST SSN STREET CITY STATE PHONE
ID NAME NAME CD
----- ---------- ----------- --------- ------------- --------- ---- ----------
1 Judi Singer 135792468 6216 Kevin Brighton MI 8102222222
2 Mega Hertz 148273423 810 Main Howell MI 5174234243
3 Judy Singer 987131312 6216 Kevin Dr Brighton MI 8102222222
4 Judi Singer 145792468 6216 Kevin Brighton MI 8102222222
5 Mega Hertz 148273423 810 Main Howell MI 5174234243
6 Junior Hertz 810 Main Howell MI 5174234243
7 Mighty Moose 184726423 333 Third St. Brighton MI 8102223423


Before I go any further, let me add that this is not my idea of a great design. In any sort of a decent prospect database, this table should be split off into at least two separate tables, and a (large) number of constraints added. This will become clear in the following discussion

(And yes, although some of the names and addresses are real, the Social Security Numbers and telephone numbers have been changed to protect the innocent).

What we are trying to have is a table which holds potential customers in some sort of marketing database. And we can start out with these questions (which relate to the whole issue of 1NF and surrogate keys): How many different prospects do we have in the database? What are the candidate keys in this table? Is it in 1NF? And we will close with these questions (which relate to a different issue): How many people is our system aware of? Suppose I added the business rule that any children in the system had to have a parent in the system?

To Be Continued:

Wednesday, January 9, 2008

ORA-12705 "invalid or unknown NLS parameter value specified"

The "Official" explanation is that the NLS parameter value specified in an alter session statement is not valid or not implemented. Except that this is not quite the entire story. If we put some nonsense in an alter session statement, Oracle is able to figure that out:


SQL> alter session set dodah = hohah;
alter session set dodah = hohah
*
ERROR at line 1:
ORA-02248: invalid option for ALTER SESSION

O.K., let's try a bad option to a good parameter:

SQL> alter session set nls_date_format = dodah;
ERROR:
ORA-01821: date format not recognized

What we need is a valid NLS parameter (such as nls_date_format, we were on the right track there) and pass it a bad option, but pick an NLS parameter which doesn't have it's own editing available (which appears to be any of them except the ones which refer to dates or time) :

SQL> alter session set nls_territory = dodah;
ERROR:
ORA-12705: invalid or unknown NLS parameter value specified

This describes how the ORA-12705 can arise in an existing session. Can one get this error during the logon process? The answer is, yes. Just set the NLS_LANG environment variable to an invalid value, and try to connect:

$ export NLS_LANG=dodah
$ echo $NLS_LANG
dodah
$ sqlplus psinger1@oradev

SQL*Plus: Release 9.2.0.8.0 - Production on Wed Oct 24 07:48:45 2007

Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved.

Enter password:
ERROR:
ORA-12705: invalid or unknown NLS parameter value specified

and NLS_LANG appears to be the only NLS parameter which can be set as an environment variable. So, in summary, if you get a 12705 and you haven't just made an obvious mistake with an alter session statement, check your NLS_LANG setting.