PRIMARY KEY and NOT NULL
I've seen this far too often. A table with a primary key (good) and a check constraint (NOT NULL) on the same column. Stop doing it. Watch. CREATE TABLE t ( id NUMBER CONSTRAINT pk_id PRIMARY KEY );
SH@I_HAVE_NO_IDEA>INSERT INTO t ( id ) VALUES ( 1 );
1 row created.
Elapsed: 00:00:00.33 SH@I_HAVE_NO_IDEA>INSERT INTO t ( id ) VALUES ( NULL ); INSERT INTO t ( id ) VALUES ( NULL ) * ERROR at line 1: ORA-01400: cannot insert NULL into ("SH"."T"."ID") As HillbillyToad said,  It is better than no constraint, that's for sure. The heart was in the right place... Labels: constraints, development, sql, wtf
Database Table Size
I've always wondered how big a table is...up until recently I depended on the DBAs to retrieve such information for me. Thanks to my good, and very helpful, friend, Mr. Thomas Roach, I no longer have to wait or bother the DBAs. %_SEGMENTS contains a column called BYTES. Use this column to determine the size of your table, with just a little math. SELECT segment_name, SUM( bytes ) / 1024 / 1024 mb FROM user_segments GROUP BY segment_name ORDER BY 1 /
SEGMENT_NAME MB ------------------------------ ---------- BMP_DIVNBR_CUST 13.375 BMP_DIVNBR_JOINFACT 37.0625 BMP_DIVNBR_PROD 13.375 BMP_DIVNBR_SALES 78.6875 CUST 940 DIV .125 IDX_CUSTSKDIVNBR_SALES 2676.6875 IDX_PRODSKDIVNBR_SALES 2701.6875 JOIN_FACT 4298.125 PIM 312 PK_DIVSK .125 PK_PIMSK 8 PK_TMSK .25 PROD 2274.4375 SALES 116122.375 TIME .875 UQ_CUSTSKDIVNBR_CUST 40.125 UQ_PRODSKDIVNBR_PROD 144 There's a plethora of these scripts out in the wild...but I was originally inspired by helping Mr. Neil Kodner out back in November (which I refer to as the "missing" month). Read his take on it here. Labels: dba, howto, sql
Dynamic Single Row Query
Once in awhile doing analysis, I'd like to see the count from each table, just to get an idea of how much data I am working with. Of course this doesn't measure width, but it is a metric. Of course I could go through and write a SELECT COUNT(*) from every table, that works if there are like 4 tables. Anything more and...well it sucks. Yes, I could gather stats on the schema and then reference NUM_ROWS, but this is an occasion where I don't have the necessary privileges. I've been trying to do this for years, with no luck, until yesterday. Typically I would do something like this: DECLARE l_count INTEGER; l_sql VARCHAR2(200); BEGIN FOR i IN ( SELECT table_name FROM dba_tables WHERE owner = 'SYSTEM' ) LOOP l_sql := 'SELECT COUNT(*) INTO :1 FROM SYSTEM.' || i.table_name; EXECUTE IMMEDIATE l_sql USING l_count, i.table_name; dbms_output.put_line( i.table_name || '-' || l_count ); END LOOP; END; / Which would of course would give me this: DECLARE * ERROR at line 1: ORA-01745: invalid host/bind variable name ORA-06512: at line 10 Only now did I realize a fatal flaw with that...I was trying to bind the variable into the string, which would obviously never work. For whatever reason, this time I decided to research it. I came across Flavio Cassetta's site and this post, SQL Error: ORA-01745: invalid host/bind variable namI'll skip a few iterations and get to the final product (because I went through this exercise on Friday night and quite honestly, don't remember all the permutations). SET SERVEROUTPUT ON
DECLARE l_count INTEGER; l_table VARCHAR2(61); BEGIN FOR i IN ( SELECT table_name FROM dba_tables WHERE owner = 'SYSTEM' ) LOOP l_table := 'SYSTEM.' || i.table_name; EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM ' || l_table INTO l_count; dbms_output.put_line( l_table || '-' || l_count ); END LOOP; END; /
...snip SYSTEM.LOGMNR_USER$-0 SYSTEM.LOGMNR_OBJ$-0 SYSTEM.LOGMNR_DICTIONARY$-0 SYSTEM.LOGMNR_DICTSTATE$-0 SYSTEM.OL$NODES-0 SYSTEM.OL$HINTS-0 SYSTEM.OL$-0
PL/SQL procedure successfully completed. Done. Labels: plsql, sql
SQL: Calculate Wasted Time
Inspired by this today:  I answered with this:  I started to do it in SQL, but then I got impatient. I finished it in Excel just to get it done. Some time later, I decided to do it in SQL...just because it's fairly easy. I know there are better/different ways to do it, so share please. SELECT ROUND( d * 10 * 14 * sal_per_minute, 2 ) yearly_cost, ROUND( ( d / 4 ) * 10 * 14 * sal_per_minute, 2 ) quarterly_cost, ROUND( ( d / 12 ) * 10 * 14 * sal_per_minute, 2 ) monthly_cost, ROUND( 5 * 10 * 14 * sal_per_minute, 2 ) weekly_cost, ROUND( 10 * 14 * sal_per_minute, 2 ) daily_cost FROM ( SELECT COUNT( CASE WHEN TO_CHAR( s, 'D' ) NOT IN ( 1, 7 ) THEN 1 ELSE NULL END ) - 10 d, MAX( sal ) sal_per_minute FROM ( SELECT TO_DATE( '31-DEC-2008', 'DD-MON-YYYY' ) + rownum s, ( 100000 / 2080 ) / 60 sal FROM dual CONNECT BY LEVEL <= 365 ) );
YEARLY_COST QUARTERLY_COST MONTHLY_COST WEEKLY_COST DAILY_COST ----------- -------------- ------------ ----------- ---------- 28157.05 7039.26 2346.42 560.9 112.18
Those minutes are precious...so don't waste them. Labels: sql
Database Tutoring
Last week a friend of mine sent over a craigslist posting, someone looking for a tutor. Here's the ad: Looking for an experienced SQL Database Systems analyst to help with homework assignments for a graduate level database course. Would like to meet 2 times per week (for 2 hours each session) over the next four months. Evening, weekends or Wednesdays preferred. The candidate must be able to explain the technical to the non-technical. Please reply with resume and availablility.
Course Topics Are: * Relational Model and Languages ( SQL) * Database Analysis and Design * Methodology (Conceptual and Logical Design) * Social, Legal, etc. Issues * Distributed DBMSs and Replication * Object DBMSs * The Web and DBMSs * Business Intelligence I replied immediately and heard back the next day. I sent my resume but I thought the blog would be more appropriate. Apparently it was enough. We spoke on Saturday for about an hour and I received all the materials necessary to start doing research including a sample database (in Access). I have to say I'm pretty excited about it. I thoroughly enjoy trying to explain database concepts so that others (non-techies) can understand. It's a Masters level class filled with students from Computer Science and from an Education Technology tract. Bet you can guess which side my "student" falls in. Seems a little odd that the Educational Technology folks are in the class, but I think it's a good thing. When they need an application in the future, they'll have a much better grasp of what to ask for and hopefully they'll be more involved in the process. I'll use this space both for reporting on progress and helping to explain things. Wish us luck! Labels: database, design, sql
SQL Developer: Drill Down Reports
Finally, finally I've figured this out. I've googled " SQL Developer Drillable Reports" to no avail. The solution kept alluding me. The first result you should get back is one from a fellow Tampan (Tampon?), Lewis Cunningham, from July 2006. OK, it's a bit old (I think it was still called Raptor back then), but I'll give it a try. In it, Lewis talks about creating additional "pseudo" columns, SDEV_LINK_NAME, SDEV_LINK_OWNER, SDEV_LINK_OBJECT which appear to map to the corresponding columns in DBA_OBJECTS.  I tried that, and got...nothing. I tried changing the alias(es) to match the column I was using, again, to no avail. Let me back up just a tad, I'm trying to create some reports based on the PLSQL_PROFILER_% tables: * PLSQL_PROFILER_RUNS * PLSQL_PROFILER_UNITS * PLSQL_PROFILER_DATA It's annoying to have to rewrite the SQL everytime. I did create a @profile script, but I had to pass the RUNID; so first, I had to know the RUNID. So I took to Twitter as I know Kris Rice hangs out there sometimes.    That was last week, and I have been unable to get this to work. I could have sworn Kris had a good tutorial on it, but I think I confused it with the extensions you can create. Anyway, I'm at it again tonight and I end up back at the link Kris originally pointed me to. For some reason (cough) I missed this crucial little nugget this first time (the bind variable is case-sensitive) Really? Could it be that easy? I UPPERed RUNID and voila! It worked! To recap, go to the Reports tab, right click on a folder (I have one named "profiler") and select Add Report.  I fill out the Name, Description and Tooltip (optional)  Hit Apply which saves my report. Now I want a report that on PLSQL_PROFILER_UNITS that accepts the RUNID as an IN parameter. First, create the report:  Go to the Binds tab and fill in the fields  Go to the Advanced tab and fill in the name of the report  Now, select your first report, right click, go to Reports and select the report you just created   Perfect! Just a small reminder, the bind parameters are CASE SENSITIVE! Labels: reports, sql, sql developer, tools
PL/SQL: Pipelined Function
As with Friday's post, pipelined functions are something I use rarely, thus end up looking up how to do it. So with a little help from Tim Hall [ blog | twitter ], I shall do a quick example. I'm trying to integrate these 2 posts, PL/SQL: Split URL Parameters and PL/SQL: Split Key-Value Pairs. I don't want to store the parsed response or request string just yet, just the string. But in the off chance I want it parsed, I don't want to load it up into excel or something and begin the arduous task of breaking it down. I will (just remembered) have to parse the response string when I receive it though to figure out whether or not the transactions was Approved or Declined. Let's start with the types: CREATE OR REPLACE TYPE r_key_value_record IS OBJECT ( orderof NUMBER(5), key_string VARCHAR2(1000), value_string VARCHAR2(1000) ) /
CREATE OR REPLACE TYPE t_key_value_table AS TABLE OF R_KEY_VALUE_RECORD / The simple function, just for demonstration purposes: CREATE OR REPLACE FUNCTION get_key_value_pairs RETURN T_KEY_VALUE_TABLE PIPELINED IS BEGIN FOR i IN 1..10 LOOP PIPE ROW ( R_KEY_VALUE_RECORD( i, i, i ) ); END LOOP; RETURN; END get_key_value_pairs; / show errors This will definitely be expanded in the days to come to include those previous posts mentioned above. I will post the final result and link it up here as well. Labels: plsql, sql, udt
REGEXP_REPLACE - Credit Card (CC) Numbers
I'm starting to rewrite some of our payment processing stuff right now. One thing I want to get a handle on is what is being sent to the payment gateway (we're capturing the response, but the request isn't easily accessible). So I created a table and I'm writing the REQUEST URL to it. Ran my first test, hey look, there is the credit card number. That won't work. I then started to think about how to mask just the credit card number...I could look for the key(word) in each URL string, but not all gateways are the same. Regular Expressions! A nice 5 year old example from Alice Rischert can be found here. Following that it was fairly easy to figure out, specifically the example (REGEXP_INSTR) on finding the Zip code. [[:digit:]]{13,16}That's it. That's all there is too it and it's applicable to each and every gateway. You could have some false positives if you're passing through 13 to 16 digit numbers, but this is essentially a logging table, so who cares. If I need to see the exact credit card number, I can look it up through the usual process (access, decrypting, etc). I ran a bunch of tests to see how it would work: VAR C VARCHAR2(4000);
SELECT regexp_replace( :c, '[[:digit:]]{13,16}', 'X' ) r FROM dual; I'm starting with the easy cases, numbers, 13 to 16 digits long: CJUSTICE@TESTING>EXEC :C := 1234123412341234; CJUSTICE@TESTING>/
R ---------------------------------------------- X
CJUSTICE@TESTING>--15 CJUSTICE@TESTING>EXEC :C := 123412341234123; CJUSTICE@TESTING>/
R ---------------------------------------------- X
CJUSTICE@TESTING>--14 CJUSTICE@TESTING>EXEC :C := 12341234123412; CJUSTICE@TESTING>/
R ---------------------------------------------- X
CJUSTICE@TESTING>--13 CJUSTICE@TESTING>EXEC :C := 1234123412341; CJUSTICE@TESTING>/
R ---------------------------------------------- X Now some of the negative cases, still just numbers: CJUSTICE@TESTING>--17 CJUSTICE@TESTING>EXEC :C := 12341234123412341; CJUSTICE@TESTING>/
R ------------------------------------------------------------ X1
CJUSTICE@TESTING>--18 CJUSTICE@TESTING>EXEC :C := 123412341234123412; CJUSTICE@TESTING>/
R ------------------------------------------------------------ X12
CJUSTICE@TESTING>--19 CJUSTICE@TESTING>EXEC :C := 1234123412341234123; CJUSTICE@TESTING>/
R ------------------------------------------------------------ X123
CJUSTICE@TESTING>--20 CJUSTICE@TESTING>EXEC :C := 12341234123412341234; CJUSTICE@TESTING>/
R ------------------------------------------------------------ X1234
CJUSTICE@TESTING>--30 CJUSTICE@TESTING>EXEC :C := 123412341234123412341234123412; CJUSTICE@TESTING>/
R ------------------------------------------------------------ XX So any number over 16 is not masked. For my purposes, I don't much care. Numbers are easy right? Let's start adding some strings in with the Credit Card number embedded in there. CJUSTICE@TESTING>SET DEFINE OFF CJUSTICE@TESTING>EXEC :C := 'card_number=1234123412341234'; CJUSTICE@TESTING>/
R ---------------------------------------------------------------------------- card_number=X
CJUSTICE@TESTING>EXEC :C := 'test=bollocks&card_number=1234123412341234'; CJUSTICE@TESTING>/
R ---------------------------------------------------------------------------- test=bollocks&card_number=X
CJUSTICE@TESTING>EXEC :C := 'card_number=1234123412341234&test=bollocks'; CJUSTICE@TESTING>/
R ---------------------------------------------------------------------------- card_number=X&test=bollocks CJUSTICE@TESTING>SET DEFINE ON This is more for me than it is for you ( another reason you should blog). I always forget how to use Regular Expressions since I encounter the opportunity to use them so rarely. Labels: regexp_replace, sql, testing
SQL: SYS_CONTEXT
In my experience so far, I've seen very few places where SYS_CONTEXT is used. It is typically used in conjunction with CONTEXTs and also Virtual Private Database (VPD). Oracle has a built in namespace called USERENV and their is a wealth of information you can retrieve from there. I wrote up a quick view to demonstrate the use of SYS_CONTEXT: SELECT SYS_CONTEXT( 'USERENV', 'ACTION' ) action, SYS_CONTEXT( 'USERENV', 'AUTHENTICATION_METHOD' ) authentication_method, SYS_CONTEXT( 'USERENV', 'BG_JOB_ID' ) bg_job_id, SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) client_identifier, SYS_CONTEXT( 'USERENV', 'CLIENT_INFO' ) client_info, SYS_CONTEXT( 'USERENV', 'CURRENT_SCHEMAID' ) current_schemaid, SYS_CONTEXT( 'USERENV', 'CURRENT_SCHEMA' ) current_schema, SYS_CONTEXT( 'USERENV', 'CURRENT_USER' ) current_user, SYS_CONTEXT( 'USERENV', 'DB_DOMAIN' ) db_domain, SYS_CONTEXT( 'USERENV', 'DB_NAME' ) db_name, SYS_CONTEXT( 'USERENV', 'DB_UNIQUE_NAME' ) db_unique_name, SYS_CONTEXT( 'USERENV', 'HOST' ) host, SYS_CONTEXT( 'USERENV', 'INSTANCE' ) instance, SYS_CONTEXT( 'USERENV', 'INSTANCE_NAME' ) instance_name, SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) ip_address, SYS_CONTEXT( 'USERENV', 'ISDBA' ) isdba, SYS_CONTEXT( 'USERENV', 'LANG' ) lang, SYS_CONTEXT( 'USERENV', 'LANGUAGE' ) language, SYS_CONTEXT( 'USERENV', 'NETWORK_PROTOCOL' ) network_protocol, SYS_CONTEXT( 'USERENV', 'NLS_CALENDAR' ) nls_calendar, SYS_CONTEXT( 'USERENV', 'NLS_CURRENCY' ) nls_currency, SYS_CONTEXT( 'USERENV', 'OS_USER' ) os_user, SYS_CONTEXT( 'USERENV', 'SERVER_HOST' ) server_host, SYS_CONTEXT( 'USERENV', 'TERMINAL' ) terminal FROM dual Which yields the following: ACTION: NULL AUTHENTICATION_METHOD: PASSWORD BG_JOB_ID: NULL CLIENT_IDENTIFIER: NULL CLIENT_INFO: NULL CURRENT_SCHEMAID: 88 CURRENT_SCHEMA: CJUSTICE CURRENT_USER: CJUSTICE DB_DOMAIN: NULL DB_NAME: testing DB_UNIQUE_NAME: testing HOST: cdj-laptop INSTANCE: 1 INSTANCE_NAME: TESTING IP_ADDRESS: 192.168.1.4 ISDBA: FALSE LANG: US LANGUAGE: AMERICAN_AMERICA.WE8MSWIN1252 NETWORK_PROTOCOL: tcp NLS_CALENDAR: GREGORIAN NLS_CURRENCY: $ OS_USER: chet SERVER_HOST: oracledb TERMINAL: UNKNOWN Basically, these are global variables that Oracle stores on the current session/user. You can create a context for just about anything you want. I've used them in the past along with table based security in a stateless environment. In essence, I create a record in a table with a SESSION_ID, I then store that in the context (global variable) and I use that to reconnect to the database each time. I promise to have an example of using it in the near future. Labels: database, oracle, plsql, security, sql
Oracle's New INSERT Syntax
Not really Oracle's...just mine. I'm busy debugging VALUES * ERROR at line 19: ORA-00947: not enough values I manually count the columns and they're the same. Run it again. VALUES * ERROR at line 19: ORA-00947: not enough values I then cut and paste both "halves" of the INSERT statement into excel so I can get a row-by-row compare. Everything looks good, go! VALUES * ERROR at line 19: ORA-00947: not enough values WTF? I still haven't figured it out, but that excel picture gave me an idea... I tend to put one column on each line, for large tables, this takes up quite a bit of vertical space. I've seen others put columns (and values) on the same line. Just looks ugly to me. Here's what this table looks like: INSERT INTO my_table ( id, create_date, update_date, col1, col2, col3, col4, col5, col6, col7, col8, col9, col10, col11, col12, col13, col14 ) VALUES seq.nextval, SYSDATE, SYSDATE, 'A', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING', 'SOMETHING' ); Wouldn't it be cool if you could do something like this though? INSERT INTO my_table ( id => seq.nexval, create_date => SYSDATE, update_date => SYSDATE, col1 => 'A', col2 => 'SOMETHING', col3 => 'SOMETHING', col4 => 'SOMETHING', col5 => 'SOMETHING', col6 => 'SOMETHING', col7 => 'SOMETHING', col8 => 'SOMETHING', col9 => 'SOMETHING', col10 => 'SOMETHING', col11 => 'SOMETHING', col12 => 'SOMETHING', col13 => 'SOMETHING', col14 => 'SOMETHING' ); 1. You'd save space. 2. It would be easier to read and 3. It would be easier to debug Thoughts? update:I did solve my problem, I was missing the opening parenthesis in the VALUES clause. update 2:I created an "Idea" over at Oracle Mix, check it out and vote for it here. Labels: oracle, sql
How To Populate Your TIME Dimension
Yesterday I wrote about the Datawarehousing TIMES table.  Today I populated it. I modified it for my owner purposes naturally, specifically removing the fiscal components (thankfully calendar year equals fiscal year). I think that saved me days of figuring out how to calculate certain fields. I remember the first time trying to do this took about 20 different SQL statements. I generated the key (date, time_id) and then calculate that either loop or perform another SQL statement using specific dates out of the table (last day of the month for instance). This time I managed to do it in a single SQL statement. Again, I left out some of the more complicated calculations since I did not have to worry about fiscal requirements. I also got a short refresher course in the date format models which always come in handy and learned a new function, NEXT_DAY. NEXT_DAY returns the date of the first weekday named by char that is later than the date date. The return type is always DATE, regardless of the datatype of date. The argument char must be a day of the week in the date language of your session, either the full name or the abbreviation. The minimum number of letters required is the number of letters in the abbreviated version. Any characters immediately following the valid abbreviation are ignored. The return value has the same hours, minutes, and seconds component as the argument date. Here's the SQL to populate your TIME Dimension: SELECT TRUNC( sd + rn ) time_id, TO_CHAR( sd + rn, 'fmDay' ) day_name, TO_CHAR( sd + rn, 'D' ) day_number_in_week, TO_CHAR( sd + rn, 'DD' ) day_number_in_month, TO_CHAR( sd + rn, 'DDD' ) day_number_in_year, TO_CHAR( sd + rn, 'W' ) calendar_week_number, ( CASE WHEN TO_CHAR( sd + rn, 'D' ) IN ( 1, 2, 3, 4, 5, 6 ) THEN NEXT_DAY( sd + rn, 'SATURDAY' ) ELSE ( sd + rn ) END ) week_ending_date, TO_CHAR( sd + rn, 'MM' ) calendar_month_number, TO_CHAR( LAST_DAY( sd + rn ), 'DD' ) days_in_cal_month, LAST_DAY( sd + rn ) end_of_cal_month, TO_CHAR( sd + rn, 'FMMonth' ) calendar_month_name, ( ( CASE WHEN TO_CHAR( sd + rn, 'Q' ) = 1 THEN TO_DATE( '03/31/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) WHEN TO_CHAR( sd + rn, 'Q' ) = 2 THEN TO_DATE( '06/30/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) WHEN TO_CHAR( sd + rn, 'Q' ) = 3 THEN TO_DATE( '09/30/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) WHEN TO_CHAR( sd + rn, 'Q' ) = 4 THEN TO_DATE( '12/31/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) END ) - TRUNC( sd + rn, 'Q' ) + 1 ) days_in_cal_quarter, TRUNC( sd + rn, 'Q' ) beg_of_cal_quarter, ( CASE WHEN TO_CHAR( sd + rn, 'Q' ) = 1 THEN TO_DATE( '03/31/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) WHEN TO_CHAR( sd + rn, 'Q' ) = 2 THEN TO_DATE( '06/30/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) WHEN TO_CHAR( sd + rn, 'Q' ) = 3 THEN TO_DATE( '09/30/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) WHEN TO_CHAR( sd + rn, 'Q' ) = 4 THEN TO_DATE( '12/31/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) END ) end_of_cal_quarter, TO_CHAR( sd + rn, 'Q' ) calendar_quarter_number, TO_CHAR( sd + rn, 'YYYY' ) calendar_year, ( TO_DATE( '12/31/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) - TRUNC( sd + rn, 'YEAR' ) ) days_in_cal_year, TRUNC( sd + rn, 'YEAR' ) beg_of_cal_year, TO_DATE( '12/31/' || TO_CHAR( sd + rn, 'YYYY' ), 'MM/DD/YYYY' ) end_of_cal_year FROM ( SELECT TO_DATE( '12/31/2002', 'MM/DD/YYYY' ) sd, rownum rn FROM dual CONNECT BY level <= 6575 ) / You can find the table definition and the INSERT statement here as well. Any feedback good, bad or ugly is welcome. Labels: datawarehouse, howto, sql
ORA-32031: illegal reference of a query name in WITH clause
I was trying to use the subquery factoring clause, WITH, in a query as it was a fairly small subset but called a number of times...with the same predicates. I ran into the above mentioned error. Here's my query: WITH periods AS ( SELECT datefrom, datethru FROM vw_periods WHERE periodtypeid = 'WEEK' AND TRUNC( SYSDATE - datefrom ) BETWEEN 0 AND 56 ) SELECT * FROM periods; (Thanks Tom!) Pretty simple right? That's what I thought. FROM vw_periods * ERROR at line 6: ORA-32031: illegal reference of a query name in WITH clause Hmmm...what's that? Since I can't get to the docs right now, I'll point you here. Cause: forward or recursive reference of a query name in WITH clause is not allowed. Action: Correct query statement, then retry. That's not very helpful. What's the definition of the view? CREATE OR REPLACE VIEW vw_periods AS SELECT periodid, periodtypeid, datefrom, datethru FROM periods; Nothing there...wait, what's the name of the table? Periods? Couldn't be that simple (and yet so obvious) could it? SQL>WITH per 2 AS 3 ( 4 SELECT datefrom, datethru 5 FROM vw_periods 6 WHERE periodtypeid = 'WEEK' 7 AND TRUNC( SYSDATE - datefrom ) BETWEEN 0 AND 56 8 ) 9 SELECT * 10 FROM per;
DATEFROM DATETHRU --------- --------- 08-DEC-08 14-DEC-08 15-DEC-08 21-DEC-08 22-DEC-08 28-DEC-08 29-DEC-08 04-JAN-09 05-JAN-09 11-JAN-09 12-JAN-09 18-JAN-09 19-JAN-09 25-JAN-09 26-JAN-09 01-FEB-09
8 rows selected. Of course it can! Lesson? Don't try to name your subquery with the same name as the base table. Labels: error, oracle, sql
Is DISTINCT a Bug?
In your application or data model that is. On a well designed system, I've rarely seen the need to use DISTINCT. If anything, I use it to do analysis on table data or maybe a summary report. Something like this: SELECT COUNT( DISTINCT( col_1 ) ) col1_count, COUNT( DISTINCT( col_2 ) ) col2_count, COUNT( DISTINCT( col_3 ) ) col3_count, COUNT(*) count_all FROM my_table; That will tell me the basic distribution of certain data elements in a specific table. Good for possibly determining whether a column needs a Bitmap Index. In an OLTP system however, the liberal use of DISTINCT signals (to me) a problem in the underlying model. Obviously not all instances are without merit, but I'd be willing to bet that the majority are an indicator. How about you? What are you thoughts on DISTINCT? Good? Bad? Indifferent? Labels: database, design, rant, sql
OBIEE: error : Odbc driver returned an error (SQLExecDirectW)
My method so far to make changes to our existing reports has been to 1. Open the report 2. Modify (Most, if not all, are Direct Database Requests) 3. Cut and Paste the SQL into my editor (JDeveloper) 4. Fix any issues 5. Run in SQL*Plus 6. Cut and paste back into OBIEE On occasion, I'll have to use the Catalog Manager, select report, properties, Edit XML and get the SQL from there. Obviously not the most efficient way, but it works. Others, like Mr. Berg, have suggested turning the log level to level 2 and capturing the SQL being passed to the database. I'm not that industrious yet. Anyway, I've run across this error a couple of times so far. When I do number 6 and click on Validate SQL and Retrieve Columns, I get the above mentioned error. Hmmm...I've just run this thing in SQL*Plus and it works just fine. The queries are really too big to simply eyeball either. So I went simple, in the text box I entered SELECT * FROM DUAL and clicked on Validate SQL and Retrieve Columns: error : Odbc driver returned an error (SQLExecDirectW). error : State: HY000. Code: 10058. [NQODBC] [SQL_STATE: HY000] [nQSError: 10058] A general error has occurred. [nQSError: 43093] An error occurred while processing the EXECUTE PHYSICAL statement. [nQSError: 17001] Oracle Error code: 933, message: ORA-00933: SQL command not properly ended at OCI call OCIStmtExecute. [nQSError: 17010] SQL statement preparation failed. (HY000) error : SQL Issued: {call NQSGetQueryColumnInfo('EXECUTE PHYSICAL CONNECTION POOL "BIDEV"."Connection Pool" SELECT * FROM DUAL when mailing_group = ''EMPLOYEE'' then ''Other'' when mailing_group = ''TEST'' then ''Other'' when mailing_group = ''VIP-EXTERNAL'' then ''Other'' when mailing_group = ''SITEUSERS'' then ''Other'' else mailing_group end )')}It appears the query that previously occupied that box is still there...partially. Mr. Berg suggested clearing the cache, so I went into the Administration tool, Manage, but cache was grayed out. I asked the DBA and Linux guy to pass me the file, but they were being difficult today, so I didn't get it. In the short term, I just created a new report appended with " R" (refactored) and moved on to the next one. Hopefully tomorrow I'll be able to get some answers. Labels: obiee, sql
SQL Objects vs. PL/SQL Tables
So I was writing a small procedure to loop through a procedure's arguments (all_arguments). After getting it working, I then began to port it to packaged code, which is where I ran into a little problem. Here's the initial statement: DECLARE TYPE r_records IS RECORD ( owner VARCHAR2(30), package_name VARCHAR2(30), procedure_name VARCHAR2(30), overload VARCHAR2(40), argument_name VARCHAR2(30), sequence INTEGER, in_out VARCHAR2(9) ); TYPE t_records IS TABLE OF R_RECORDS INDEX BY BINARY_INTEGER; l_records T_RECORDS; BEGIN SELECT owner, package_name, object_name, overload, argument_name, sequence, in_out BULK COLLECT INTO l_records FROM dba_arguments WHERE package_name = 'DBMS_UTILITY' AND object_name IN ( 'COMPILE_SCHEMA', 'INVALIDATE' ) ORDER BY owner, package_name, object_name, position; END; / Runs fine. My first step to refactor was to use the TYPE declaration in the package header. But you can't do that (little rusty on pl/sql tables). So I created SQL Objects or User Defined Types (UDT). CREATE TYPE r_procedure_arguments AS OBJECT ( owner VARCHAR2(30), package_name VARCHAR2(30), procedure_name VARCHAR2(128), overload VARCHAR2(40), argument_name VARCHAR2(30), position INTEGER, sequence INTEGER, in_out VARCHAR2(9) ); / show errors
CREATE TYPE t_procedure_arguments AS TABLE OF R_PROCEDURE_ARGUMENTS; / show errors Then I rewrote the anonymous block to use the UDT. CJUSTICE@ELEVEN>DECLARE 2 l_records T_PROCEDURE_ARGUMENTS; 3 BEGIN 4 SELECT 5 owner, 6 package_name, 7 object_name, 8 overload, 9 argument_name, 10 position, 11 sequence, 12 in_out 13 BULK COLLECT INTO l_records 14 FROM all_arguments; 15 END; 16 / FROM all_arguments; * ERROR at line 14: ORA-06550: line 14, column 3: PL/SQL: ORA-00947: not enough values ORA-06550: line 4, column 3: PL/SQL: SQL Statement ignored not enough values? OK, let's go through it. There are 8 "columns" in the defined object and 8 in the SELECT clause. Hmmm...I tried adding some extra values at the end: DECLARE l_records T_PROCEDURE_ARGUMENTS; BEGIN SELECT owner, package_name, object_name, overload, argument_name, position, sequence, in_out, 1, 2, 3, 4 BULK COLLECT INTO l_records FROM all_arguments; END; / Same result, not enough values. I spent the next hour toggling between gmail, facebook and meebo thinking about the problem. Wait! You have to "cast" the values from the SELECT clause! CJUSTICE@ELEVEN>DECLARE 2 l_records T_PROCEDURE_ARGUMENTS; 3 BEGIN 4 SELECT 5 R_PROCEDURE_ARGUMENTS( owner, 6 package_name, 7 object_name, 8 overload, 9 argument_name, 10 position, 11 sequence, 12 in_out ) 13 BULK COLLECT INTO l_records 14 FROM all_arguments; 15 END; 16 /
PL/SQL procedure successfully completed. Duh. So I post it here so next time I don't waste an hour trying to remember why I can't get it to work. Labels: development, plsql, sql, udt
Index those Foreign Keys
I've been reading about this phenomenon for years over on asktom, but I had never actually encountered the problem, until today that is. I'm helping out doing a small piece on another project. Trying to get back into the habit of "good" unit-testing, I have created some test data. One build script that runs the whole thing, a few user scripts and finally a teardown script that deletes all the data I have created. Naturally, I run it via SQL*Plus; turning feedback and echo off and using a liberal number of PROMPT directives (very similar to how the ApEx install goes coincidentally). This is what my teardown script reports: ...deleting from child table 1 ...deleting from child table 2 ...deleting from child table 3 ...etc, etc Nothing fancy. Then I realized on the final DELETE it was hanging up. Any locked objects? Nope. Maybe it's the trigger? I checked, only saw ON INSERT OR UPDATE, confirmed that with another developer. He suggested unindexed foreign keys. Huh? Really? That's impossible you say. It wasn't impossible. It was true. So I ran my copy of the Tom's " find unindexed foreign keys." Quite a few "****" which is not good. So I went searching for them myself and came up with this little query (for my specifc table): SELECT b.table_owner, b.table_name, b.constraint_name, b.column_name, a.index_name FROM dba_ind_columns a, ( SELECT a.owner table_owner, a.table_name, a.constraint_name, b.column_name FROM dba_constraints a, dba_cons_columns b WHERE a.constraint_name = b.constraint_name AND a.owner = b.owner AND a.table_name = b.table_name AND a.r_constraint_name = ( SELECT constraint_name FROM DBA_CONSTRAINTS WHERE TABLE_NAME = 'MY_TABLE' AND owner = 'SCHEMA_NAME' AND constraint_type = 'P' ) ) b WHERE b.table_owner = a.table_owner (+) AND b.column_name = a.column_name (+) AND b.table_name = a.table_name (+) ORDER BY b.table_name, b.constraint_name
That gave me a list of all the columns referencing the primary key (some 37 different tables). If index_name was NULL, then I knew I had to add one. Since I have this obsession lately with generating code, I just used this query and added the following: ( CASE WHEN a.index_name IS NULL THEN 'CREATE INDEX schema_name.' || b.table_name || '_' || b.column_name || '_idx ON ' || b.table_owner || '.' || b.table_name || '( ' || b.column_name || ' ) TABLESPACE my_tablespace;' END ) sql_text Now I have my CREATE INDEX statements and all is well. I run the teardown script again and it finishes like it should. Labels: constraints, indexes, sql, testing
Things I've Learned This Week
You can in fact DELETE from DBA_JOBS. Not that I had ever tried before this weekend, I just assumed it was a view (still might be with an INSTEAD OF trigger). I also learned how to reset a sequence without dropping and recreating it. This was courtesy of my crazy DBA, oraclue. Example: SQL> CREATE SEQUENCE TEST_SEQ 2 START WITH 10 3 INCREMENT BY 10 4 MINVALUE -1000;
Sequence created.
SQL> SELECT test_seq.nextval FROM dual CONNECT BY LEVEL <= 10;
NEXTVAL ---------- 10 20 30 40 50 60 70 80 90 100
10 rows selected.
SQL> SELECT * FROM user_sequences WHERE sequence_name = 'TEST_SEQ';
SEQUENCE MIN_VALUE MAX_VALUE INCREMENT_BY C O CACHE_SIZE LAST_NUMBER -------- ---------- ---------- ------------ - - ---------- ----------- TEST_SEQ -1000 1.0000E+27 10 N N 20 210
SQL> ALTER SEQUENCE TEST_SEQ INCREMENT BY -10;
Sequence altered.
SQL> SELECT * FROM user_sequences WHERE sequence_name = 'TEST_SEQ';
SEQUENCE MIN_VALUE MAX_VALUE INCREMENT_BY C O CACHE_SIZE LAST_NUMBER -------- ---------- ---------- ------------ - - ---------- ----------- TEST_SEQ -1000 1.0000E+27 -10 N N 20 90
SQL> SELECT test_seq.nextval FROM dual CONNECT BY LEVEL <= 10;
NEXTVAL ---------- 90 80 70 60 50 40 30 20 10 0
10 rows selected.
SQL> SELECT * FROM user_sequences WHERE sequence_name = 'TEST_SEQ';
SEQUENCE MIN_VALUE MAX_VALUE INCREMENT_BY C O CACHE_SIZE LAST_NUMBER -------- ---------- ---------- ------------ - - ---------- ----------- TEST_SEQ -1000 1.0000E+27 -10 N N 20 -110
SQL> ALTER SEQUENCE TEST_SEQ INCREMENT BY 10;
Sequence altered.
SQL> SELECT * FROM user_sequences WHERE sequence_name = 'TEST_SEQ';
SEQUENCE MIN_VALUE MAX_VALUE INCREMENT_BY C O CACHE_SIZE LAST_NUMBER -------- ---------- ---------- ------------ - - ---------- ----------- TEST_SEQ -1000 1.0000E+27 10 N N 20 10
SQL> SELECT test_seq.nextval FROM dual CONNECT BY LEVEL <= 10;
NEXTVAL ---------- 10 20 30 40 50 60 70 80 90 100
10 rows selected. Labels: oracle, sequences, sql
Fun with SQL: Analytics and Heirarchical
I've had this problem since yesterday and I believe I finally solved it. Given this data: ID DIFF_ID START_DAT END_DATE AMOUNT --------- ---------- --------- --------- ---------- 1 4 01-JAN-08 31-JAN-08 40 2 4 01-FEB-08 29-FEB-08 0 3 4 01-MAR-08 31-MAR-08 10 4 4 01-APR-08 30-APR-08 10 5 4 01-MAY-08 31-MAY-08 0 6 1 01-JAN-08 31-JAN-08 10 7 1 01-FEB-08 29-FEB-08 0 8 1 01-MAR-08 31-MAR-08 10 9 1 01-APR-08 30-APR-08 10 10 1 01-MAY-08 31-MAY-08 10 For each consecutive time period (month) that there is an amount, count how many buckets, up to six. First thought was definitely Analytics. I toiled away on what became a very unwieldy query (took more than one page anyway). A whole bunch of LAGs with the same number of ever increasing CASE statements. My first obstacle overcome was to filter out those that had a 0 for amount. That left me with: ID DIFF_ID START_DAT END_DATE AMOUNT ---------- ---------- --------- --------- ---------- 1 4 01-JAN-08 31-JAN-08 40 3 4 01-MAR-08 31-MAR-08 10 4 4 01-APR-08 30-APR-08 10 6 1 01-JAN-08 31-JAN-08 10 8 1 01-MAR-08 31-MAR-08 10 9 1 01-APR-08 30-APR-08 10 10 1 01-MAY-08 31-MAY-08 10 It took a good while to figure that out for some reason. Once I had that figured, I needed to figure out which were consecutive. Frank Zhou is always solving puzzles with SQL and I remembered I had responded to one of his about a year ago. If you get a chance, please take a look at his site...he solves some pretty cool puzzles with SQL using the MODEL clause and analytics. Anyway, his post, How to find the earliest start date and the latest end date for consecutive transactions in SQL was similar (and my response similar), so I found it to revisit my thinking at the time. First, I use the LAG function to get the previous row's ID (unique) and call it PREV_ID. I use DIFF_ID in the PARTITION clause (window) and order by END_DATE; then add one to see if the months are consecutive. If that value matches the START_DATE of the current row, it's consecutive and I use LAG again to get the previous row's ID. SELECT diff_id, id, start_date, end_date, ( CASE WHEN LAG( end_date ) OVER ( PARTITION BY diff_id ORDER BY end_date ASC ) + 1 = start_date THEN LAG( id ) OVER ( PARTITION BY diff_id ORDER BY end_date ASC ) ELSE NULL END ) prev_id, amount FROM col_test WHERE amount > 0 That produces the following output: DIFF_ID ID START_DAT END_DATE PREV_ID AMOUNT -------- ---------- --------- --------- ---------- ---------- 1 6 01-JAN-08 31-JAN-08 10 1 8 01-MAR-08 31-MAR-08 10 1 9 01-APR-08 30-APR-08 8 10 1 10 01-MAY-08 31-MAY-08 9 10 4 1 01-JAN-08 31-JAN-08 40 4 3 01-MAR-08 31-MAR-08 10 4 4 01-APR-08 30-APR-08 3 10 As you can see, I have 3 records with the PREV_ID populated. As I am building it, I realize I keep nesting the queries, so in comes the WITH clause (when I first learned of that it was terribly difficult to search for, I didn't know it was also called subquery factoring clause...). WITH sub AS ( SELECT diff_id, id, start_date, end_date, ( CASE WHEN LAG( end_date ) OVER ( PARTITION BY diff_id ORDER BY end_date ASC ) + 1 = start_date THEN LAG( id ) OVER ( PARTITION BY diff_id ORDER BY end_date ASC ) ELSE NULL END ) prev_id, amount FROM col_test WHERE amount > 0 ) SELECT diff_id, id, start_date, end_date, TO_DATE( SUBSTR( SYS_CONNECT_BY_PATH( TO_CHAR( start_date, 'MMDDYYYY' ), '-' ), 2, 8 ), 'MMDDYYYY' ) min_start_date FROM sub START WITH prev_id IS NULL CONNECT BY PRIOR id = prev_id Much better. Note the START WITH and CONNECT BY PRIOR, I created my own heirarchical table to determine another window to PARTITION on (MIN_START_DATE of the consecutive records). DIFF_ID ID START_DAT END_DATE MIN_START ------- ---------- --------- --------- --------- 1 6 01-JAN-08 31-JAN-08 01-JAN-08 1 8 01-MAR-08 31-MAR-08 01-MAR-08 1 9 01-APR-08 30-APR-08 01-MAR-08 1 10 01-MAY-08 31-MAY-08 01-MAR-08 4 1 01-JAN-08 31-JAN-08 01-JAN-08 4 3 01-MAR-08 31-MAR-08 01-MAR-08 4 4 01-APR-08 30-APR-08 01-MAR-08 Now all I have to do is PIVOT the table (I chose not to use the new PIVOT feature) on DIFF_ID and add an analytic COUNT on my new window (MIN_START_DATE). WITH sub AS ( SELECT diff_id, id, start_date, end_date, ( CASE WHEN LAG( end_date ) OVER ( PARTITION BY diff_id ORDER BY end_date ASC ) + 1 = start_date THEN LAG( id ) OVER ( PARTITION BY diff_id ORDER BY end_date ASC ) ELSE NULL END ) prev_id, amount FROM col_test WHERE amount > 0 ) SELECT diff_id, COUNT( CASE WHEN d = 1 THEN 1 ELSE NULL END ) b1, COUNT( CASE WHEN d = 2 THEN 1 ELSE NULL END ) b2, COUNT( CASE WHEN d = 3 THEN 1 ELSE NULL END ) b3, COUNT( CASE WHEN d = 4 THEN 1 ELSE NULL END ) b4, COUNT( CASE WHEN d = 5 THEN 1 ELSE NULL END ) b5, COUNT( CASE WHEN d = 6 THEN 1 ELSE NULL END ) b6 FROM ( SELECT diff_id, COUNT( id ) OVER ( PARTITION BY diff_id, SUBSTR( SYS_CONNECT_BY_PATH( TO_CHAR( start_date, 'MMDDYYYY' ), '-' ), 2, 8 ) ORDER BY end_date ) d FROM sub START WITH prev_id IS NULL CONNECT BY PRIOR id = prev_id ) GROUP BY diff_id; And voila! DIFF_ID B1 B2 B3 B4 B5 B6 ------- ---- ---- ---- ---- ---- ---- 1 2 1 1 0 0 0 4 2 1 0 0 0 0 Problem solved! Table creation and data can be found here. Labels: sql
Calculate Total Possible Score for Wii Bowling, Power Throws
Needless to say I have too much time on my hands. I've become obsessed with two Wii games, Bowling and Tanks. I play over and over. I had a perfect game in bowling the other day which wasn't quite as exciting as I thought it would be. I've also been playing the "Training" game of Power Throws (bowling). You are given 10 shots with another row added each frame. You start out with 10, then 15, then 21, etc. If you get all the pins, you get a bonus (total pins * 2). So far, my highest score has been 651. I've never bothered to figure out what the total possible score is though. SQL to the rescue. Using analytics and the CONNECT BY LEVEL option (10g and higher), it's fairly easy: SELECT pin_row, pins, running_total, rt_with_bonus FROM ( SELECT rownum pin_row, rownum pins, SUM( rownum ) OVER ( ORDER BY rownum ) running_total, ( SUM( rownum ) OVER ( ORDER BY rownum ) * 2 ) rt_with_bonus FROM dual CONNECT BY LEVEL <= 13 ) WHERE pins >= 4
I had to set the filter on the outer query because you start with 4 rows (10 pins). And the results: CJUSTICE@XE>BREAK ON REPORT CJUSTICE@XE>COMPUTE SUM OF RUNNING_TOTAL ON REPOR CJUSTICE@XE>COMPUTE SUM OF RT_WITH_BONUS ON REPOR CJUSTICE@XE>/
PIN_ROW PINS RUNNING_TOTAL RT_WITH_BONUS ---------- ---------- ------------- ------------- 4 4 10 20 5 5 15 30 6 6 21 42 7 7 28 56 8 8 36 72 9 9 45 90 10 10 55 110 11 11 66 132 12 12 78 156 13 13 91 182 ------------- ------------- sum 445 890
Voila! 890 is the best possible score if I strike every frame. I did manage to pass my high score by 9 pins tonight as well. It's a great day! So how come no one wants to hire a guy that can do this kind of fun stuff with SQL? ;) Labels: funny, puzzle, sql
Oracle Mix: Two Ideas
I've added two ideas at Oracle Mix. The first one is UTL_FILE has the ability to read the contents of a directory and the second one is Add a pseudo-column that stores the date the record was INSERTed or UPDATEd. I mentioned the second one before. The first one I've seen on the iloveplsqland.net site. So go and vote (or don't, but leave your comments). Labels: development, ideas, oracle, oracle mix, sql
11g New Feature: PIVOT
I do have an interview tomorrow. Woohoo! So my go at the PIVOT operator, "new" in 11g. pivot_clauseDatawarehouse GuideSQL Reference ExamplesArup Nanda's ExampleMy example. Let's create some data first: CREATE TABLE transaction_types ( transactiontypecode VARCHAR2(10) CONSTRAINT pk_transactiontypecode PRIMARY KEY );
INSERT INTO transaction_types( transactiontypecode ) VALUES ( 'DEBIT' ); INSERT INTO transaction_types( transactiontypecode ) VALUES ( 'CREDIT' );
CREATE TABLE transactions ( id NUMBER(10) CONSTRAINT pk_id PRIMARY KEY, transactiontypecode CONSTRAINT fk_ttcode_transactions REFERENCES transaction_types( transactiontypecode ) CONSTRAINT nn_ttcode_transactions NOT NULL, amount NUMBER(16,2) CONSTRAINT nn_amount_trans NOT NULL CONSTRAINT ck_amount_trans CHECK ( amount >= 0 ), date_created DATE DEFAULT SYSDATE CONSTRAINT nn_datecreated_trans NOT NULL );
INSERT INTO transactions ( id, transactiontypecode, amount ) VALUES ( 1, 'DEBIT', 44.44 );
INSERT INTO transactions ( id, transactiontypecode, amount ) VALUES ( 2, 'DEBIT', 20.34 );
INSERT INTO transactions ( id, transactiontypecode, amount ) VALUES ( 3, 'CREDIT', 5.60 );
INSERT INTO transactions ( id, transactiontypecode, amount ) VALUES ( 4, 'DEBIT', 67 );
INSERT INTO transactions ( id, transactiontypecode, amount ) VALUES ( 5, 'DEBIT', 234.55 );
INSERT INTO transactions ( id, transactiontypecode, amount ) VALUES ( 6, 'CREDIT', 76.55 );
INSERT INTO transactions ( id, transactiontypecode, amount ) VALUES ( 7, 'DEBIT', 3.45 );
So what's the big deal with PIVOT? I'm not sure yet other than it's something new and new is cool. Basically, PIVOT allows you to pivot rows into columns. We often do this for reports we generate. Here's the old way: SELECT TRUNC( date_created ) date_created, SUM( CASE WHEN transactiontypecode = 'DEBIT' THEN amount END ) debit_amount, SUM( CASE WHEN transactiontypecode = 'CREDIT' THEN amount END ) credit_amount FROM transactions GROUP BY TRUNC( date_created ) ORDER BY date_created;
DATE_CREA DEBIT_AMOUNT CREDIT_AMOUNT --------- ------------ ------------- 31-JUL-08 369.78 82.15
Really, not that bad. But if I want to do COUNT and AVG, I have to create more CASE statements like the ones above. My query will go from 14 lines to 30 in a hurry. Here's the new cool way: SELECT * FROM ( SELECT transactiontypecode tt, TRUNC( date_created ) date_created, amount FROM transactions ) PIVOT ( SUM( amount ) total_amount FOR tt IN ( 'DEBIT' AS "DEBIT", 'CREDIT' AS "CREDIT" ) );
DATE_CREA DEBIT_TOTAL_AMOUNT CREDIT_TOTAL_AMOUNT --------- ------------------ ------------------- 31-JUL-08 369.78 82.15
It happens that is 14 rows as well. Now I'll add AVG and COUNT: SELECT * FROM ( SELECT transactiontypecode tt, TRUNC( date_created ) date_created, amount FROM transactions ) PIVOT ( SUM( amount ) total_amount, COUNT( amount ) total_count, AVG( amount ) avg_amount FOR tt IN ( 'DEBIT' AS "DEBIT", 'CREDIT' AS "CREDIT" ) );
DATE_CREA D_AMT D_CNT D_AVG C_AMT C_CNT C_AVG --------- ---------- ---------- ---------- ---------- ---------- ---------- 31-JUL-08 369.78 5 73.956 82.15 2 41.075
Nice! Sixteen lines of SQL...not bad at all. That should make code a bit more readable (it'll fit on a single page)...I like it! Labels: 11g, development, sql
Commas: Before or After the Line?
I'm in a new group again which means I have to learn (and accept) other people's style. No one at WellCare put commas before the line (thankfully), but I've found a few here. I've finally come to accept that this is just style and doesn't really matter, as long as the code does what it's intended to do. Yes, it's silly, but we all have our little quirks right? Labels: rant, sql, style
Count The K's
I have this really annoying co-worker who happens to be the DBA. Everytime he walks by my desk he pounces on my keyboard. I've learned to Windows Key + L to lock the computer when I hear him approaching, but occasionally I forget. As I'm standing near my unlocked computer he starts typing furiously in my sql*plus session (shouldn't he know better?). I should know better... Walking away he asks, "How many K's are in there?" I ignored him, but then wondered myself...what's the best solution to this problem? So, here it goes: DECLARE l_count_k NUMBER := 0; l_string VARCHAR2(300); l_string_length INTEGER; BEGIN l_string := 'W34 6OKWE;KTL;SERT;LKSERTSLKRLTKRTKL;KERTL;ERKT;LKERLTKERKT;EKT;LEKRT;LEK; LTKERTKERKT;LEKRT;LEKRT;LER;LTKETK;ERKT;LEKRT;LEKT;LEKR;LTKETKERTKL;ERKTL;ERKTL;ET;LEKT;LEKRT;LERTL; EKRL;TKELTERTKL;ETL;EKTL;EKTL;ETL;ERTL;EL;TERTRTL;ERTL;ERTL;ERTL;EKRT;LEKTL;EKT;LERTL;EKL;KER;LTKE;L TKELTKER';
l_string_length := LENGTH( l_string );
FOR i IN 1..l_string_length LOOP IF SUBSTR( l_string, i, 1 ) IN ( 'K', 'k' ) THEN l_count_k := l_count_k + 1; END IF; END LOOP; dbms_output.put_line( 'Kk Count: ' || l_count_k ); END; /
Easy enough, 45. Then I started thinkinhg...can I do this in pure SQL? Of course! SELECT SUM( CASE WHEN SUBSTR( UPPER( mystring ), rownum, 1 ) = 'K' THEN 1 END ) k FROM dual, ( SELECT 'W34 6OKWE;KTL;SERT;LKSERTSLKRLTKRTKL;KERTL;ERKT;LKERLTKERKT;EKT;LEKRT;LEK; LTKERTKERKT;LEKRT;LEKRT;LER;LTKETK;ERKT;LEKRT;LEKT;LEKR;LTKETKERTKL;ERKTL;ERKTL;ET;LEKT;LEKRT;LERTL; EKRL;TKELTERTKL;ETL;EKTL;EKTL;ETL;ERTL;EL;TERTRTL;ERTL;ERTL;ERTL;EKRT;LEKTL;EKT;LERTL;EKL;KER;LTKE;L TKELTKER' mystring FROM dual ) c CONNECT BY LEVEL <= LENGTH( mystring )
COUNT_OF_K ---------- 45
1 row selected. I'm sure many of you can do better than that. So let's see 'em. Prodlife, this isn't a complicators test either. ;) Labels: funny, puzzle, sql
SQL for Buying a New Car
Two weeks ago I purchased a new car. The main reason behind that was that I drove a 1996 Nissan P.O.S. (not point of sale either). I now had a 30+ mile commute each way and I (well, my wife and mother) needed a reliable car. A big factor was gas mileage. Two brands stood out, Honda and Toyota. Honda has the Civic Hybrid and Toyota has the Prius (even though I don't care much for the design). The Corolla and Civic were the top two cars. So after visiting those two dealers to look at cars, I stopped by the Mazda dealer to see if they had anything with MPG greater than 30. I drove the Mazda3 and it was nice (everything would be nice compared to my POS). Salesman offered to run the numbers and I said OK. The point of this rambling, is that I want to know what it will cost based on mileage. Of course I didn't have my laptop, so I couldn't run the numbers (and I hadn't previously). I did it in Excel, but wanted to try it out in SQL. Here's the result: COLUMN ppg FORMAT $999.00 COLUMN car_1_gpy FORMAT 99,999 COLUMN car_2_gpy FORMAT 99,999 COLUMN car_1_cpy FORMAT $999,999.00 COLUMN car_2_cpy FORMAT $999,999.00 COLUMN car_1_cpm FORMAT 999.00 COLUMN car_2_cpm FORMAT 999.00
SELECT car_1, car_2, ppg, mpy, ROUND( ( mpy / car_1 ) ) car_1_gpy, ROUND( ( mpy / car_2 ) ) car_2_gpy, ROUND( ( ( mpy / car_1 ) * ppg ), 2 ) car_1_cpy, ROUND( ( ( mpy / car_2 ) * ppg ), 2 ) car_2_cpy, ROUND( ( ( mpy / car_1 ) * ppg ) / mpy, 2 ) car_1_cpm, ROUND( ( ( mpy / car_2 ) * ppg ) / mpy, 2 ) car_2_cpm FROM ( SELECT rownum car_1, LAG( rownum, 5 ) OVER ( PARTITION BY NULL ORDER BY rownum ) car_2 FROM dual CONNECT BY LEVEL <= 50 ) car_miles, ( SELECT 3.99 ppg, 15000 mpy FROM dual ) constants /
CAR_1 CAR_2 CAR_1_GPY CAR_2_GPY CAR_1_CPY CAR_2_CPY CAR_1_CPM CAR_2_CPM ---------- ---------- --------- --------- ------------ ------------ --------- --------- 1 15,000 $59,850.00 3.99 2 7,500 $29,925.00 2.00 3 5,000 $19,950.00 1.33 4 3,750 $14,962.50 1.00 5 3,000 $11,970.00 .80 6 1 2,500 15,000 $9,975.00 $59,850.00 .67 3.99 7 2 2,143 7,500 $8,550.00 $29,925.00 .57 2.00 8 3 1,875 5,000 $7,481.25 $19,950.00 .50 1.33 9 4 1,667 3,750 $6,650.00 $14,962.50 .44 1.00 10 5 1,500 3,000 $5,985.00 $11,970.00 .40 .80 11 6 1,364 2,500 $5,440.91 $9,975.00 .36 .67 12 7 1,250 2,143 $4,987.50 $8,550.00 .33 .57 13 8 1,154 1,875 $4,603.85 $7,481.25 .31 .50 14 9 1,071 1,667 $4,275.00 $6,650.00 .28 .44 15 10 1,000 1,500 $3,990.00 $5,985.00 .27 .40 16 11 938 1,364 $3,740.63 $5,440.91 .25 .36 17 12 882 1,250 $3,520.59 $4,987.50 .23 .33 18 13 833 1,154 $3,325.00 $4,603.85 .22 .31 19 14 789 1,071 $3,150.00 $4,275.00 .21 .28 20 15 750 1,000 $2,992.50 $3,990.00 .20 .27 21 16 714 938 $2,850.00 $3,740.63 .19 .25 22 17 682 882 $2,720.45 $3,520.59 .18 .23 23 18 652 833 $2,602.17 $3,325.00 .17 .22 24 19 625 789 $2,493.75 $3,150.00 .17 .21 25 20 600 750 $2,394.00 $2,992.50 .16 .20 26 21 577 714 $2,301.92 $2,850.00 .15 .19 27 22 556 682 $2,216.67 $2,720.45 .15 .18 28 23 536 652 $2,137.50 $2,602.17 .14 .17 29 24 517 625 $2,063.79 $2,493.75 .14 .17 30 25 500 600 $1,995.00 $2,394.00 .13 .16 31 26 484 577 $1,930.65 $2,301.92 .13 .15 32 27 469 556 $1,870.31 $2,216.67 .12 .15 33 28 455 536 $1,813.64 $2,137.50 .12 .14 34 29 441 517 $1,760.29 $2,063.79 .12 .14 35 30 429 500 $1,710.00 $1,995.00 .11 .13 36 31 417 484 $1,662.50 $1,930.65 .11 .13 37 32 405 469 $1,617.57 $1,870.31 .11 .12 38 33 395 455 $1,575.00 $1,813.64 .11 .12 39 34 385 441 $1,534.62 $1,760.29 .10 .12 40 35 375 429 $1,496.25 $1,710.00 .10 .11 41 36 366 417 $1,459.76 $1,662.50 .10 .11 42 37 357 405 $1,425.00 $1,617.57 .09 .11 43 38 349 395 $1,391.86 $1,575.00 .09 .11 44 39 341 385 $1,360.23 $1,534.62 .09 .10 45 40 333 375 $1,330.00 $1,496.25 .09 .10 46 41 326 366 $1,301.09 $1,459.76 .09 .10 47 42 319 357 $1,273.40 $1,425.00 .08 .09 48 43 313 349 $1,246.88 $1,391.86 .08 .09 49 44 306 341 $1,221.43 $1,360.23 .08 .09 50 45 300 333 $1,197.00 $1,330.00 .08 .09
Obviously this isn't terribly difficult. You can do lots with the results as well. Three or four years ago I would have created a table and a function to do this. Now I can do it "virtually." I love SQL... *Key: car_1 = miles for car 1 car_2 = miles for car 2 car_1_gpy = gallons per year car 1 car_2_gpy = gallons per year car 2 car_1_cpy = cost per year car 1 (gas) car_2_cpy = cost per year car 2 (gas) car_1_cpm = cost per mile car 1 car_2_cpm = cost per mile car 2 Labels: data, sql
BULK COLLECT and FORALL
I have a task to tune a little bit of SQL. It's very ETL like, but the target is not (yet) a star schema, it's pretty much a table flattened out for reporting purposes. As I reviewed it, I noticed it went row by row, with a COMMIT inside the LOOP. That's gotta go. Can I do this in one SQL statement? No, there's other processing that needs to be done (UPDATE two other tables before and after). Hmmm...could I just return the appropriate records into a collection? I'll have to look at that to see if it's possible. For now though, I am going to try and use BULK COLLECT with the LIMIT clause and FORALL for the processes that occur before and after. Let's create some data: CREATE TABLE t( x NUMBER, y NUMBER );
INSERT INTO t( x, y ) SELECT TRUNC( dbms_random.value( 1, 99999999 ) ), TRUNC( dbms_random.value( 1, 100000 ) ) FROM dual CONNECT BY level < 1001;
OK, now let's create an anonymous block, BULK COLLECTing the data from T into a PL/SQL table and then populated another table with that data: DECLARE TYPE t_record IS TABLE OF T%ROWTYPE; l_table T_RECORD; CURSOR c IS SELECT x, y FROM t; BEGIN OPEN c;
LOOP FETCH c BULK COLLECT INTO l_table LIMIT 100;
FORALL i IN 1..l_table.COUNT INSERT INTO s ( x, y ) VALUES ( l_table(i).x, l_table(i).y );
EXIT WHEN C%NOTFOUND; END LOOP; CLOSE c; END; /
And then I run it and I get the following: ERROR at line 18: ORA-06550: line 18, column 16: PLS-00436: implementation restriction: cannot reference fields of BULK In-BIND table of records ORA-06550: line 18, column 16: PLS-00382: expression is of wrong type ORA-06550: line 18, column 30: PLS-00436: implementation restriction: cannot reference fields of BULK In-BIND table of records ORA-06550: line 18, column 30: PLS-00382: expression is of wrong type ORA-06550: line 18, column 16: PL/SQL: ORA-22806: not an object or REF ORA-06550: line 17, column 7: PL/SQL: SQL Statement ignored
A quick google search and I end up here . So I can bulk bind, but I have to INSERT into the table as a whole. I can't be selective. I updated my code to this: DECLARE TYPE t_record IS TABLE OF T%ROWTYPE; l_table T_RECORD; CURSOR c IS SELECT x, y FROM t; BEGIN OPEN c;
LOOP FETCH c BULK COLLECT INTO l_table LIMIT 100;
FORALL i IN 1..l_table.COUNT INSERT INTO s VALUES l_table(i);
EXIT WHEN C%NOTFOUND; END LOOP; CLOSE c; END; /
I run it and it completes successfully. This is all on XE, so I wonder (hope) that 11g will allow me to do what I want (I'll be working on an 11g RAC system). I scroll down the list of google results and I find this one which then takes me to AskTom. The first post demonstrates that my first attempt will work on 11g. BULK COLLECT and FORALL are great tools if you can't do it in a single SQL statement and if you want to avoid the row by row processing. Labels: development, howto, oracle, plsql, sql
Multi Table INSERTs
I learned something new today! Multi Table INSERTs. I was trying to capture the new records into a staging table using RETURNING BULK COLLECT INTO, but that doesn't work (yet). I remembered reading about Multi Table INSERTs, but never had the need for using them, so never bothered to learn it. CREATE TABLE t ( x NUMBER PRIMARY KEY, y NUMBER, z NUMBER );
CREATE TABLE s ( x NUMBER PRIMARY KEY );
INSERT ALL INTO t ( x, y, z ) VALUES ( myrownum, ran1, ran2 ) INTO s ( x ) VALUES ( myrownum ) SELECT rownum myrownum, dbms_random.value ran1, dbms_random.value ran2 FROM dual CONNECT BY LEVEL < 101;
CJUSTICE@ORA10GR2>SELECT COUNT(*) FROM t;
COUNT(*) ---------- 100
1 row selected.
Elapsed: 00:00:00.01 CJUSTICE@ORA10GR2>SELECT COUNT(*) FROM s;
COUNT(*) ---------- 100
1 row selected.
Elapsed: 00:00:00.02
Awesome! Labels: database, datawarehouse, sql
Validating a Process Part II
Continued from my previous post. While discussing external tables with my feisty colleague some time back, I explained that I liked using them but I couldn't figure out how to change the file name to match that of what was defined in the table definition. Colleage to the rescue: ALTER TABLE table_name LOCATION ( 'new_file_name.csv' );
Cool! As I mentioned before, I had 4 files types I had to read: 820 and 835, both of the x12 format and two custom file layouts (flat files essentially). Since no one in the group knows Java yet, I wanted to keep the Java portion of the application as small as possible. So with the two custom files, I decided to use external tables. I could then put into practice the above ALTER TABLE statement. As I looped through the list of files to be processed, I would issue an EXECUTE IMMEDIATE so that I could then SELECT from the table in the next step. It worked like a charm. As I was doing some testing, I would issue the ROLLBACK statement to clear the tables for the next run. When I verified, there was still data there. WTF? Oh wait, there's an EXECUTE IMMEDIATE...which runs DDL...which COMMITs...barnacles! So I couldn't use that new thing I learned, oh well. Fortunately UTL_FILE does have the ability to rename files so I picked a name like 'external_table_file_name.txt' and rename the incoming file to that, then SELECT. Works like a charm. Labels: ddl, java, sql, utl_file
It's a Matter of Time
I've been thinking a lot lately about my recent failed deployments. How did I get so sloppy? I'm not one to make excuses, but I would say there are some mitigating circumstances at least. Time being one of them. So I got out my trusty calculator (SQL*Plus), and ran the numbers. From August 26, 2007 through March 30, 2008, I've worked 1802 hours. Of that, 118 were PTO or holiday, which brings the total down to 1784. For perspective, a work year of 8 hours per day comes out to 2080 hours a year. VAR HOURS NUMBER; EXEC :hours := 1784
1 SELECT 2 ROUND( ( :HOURS / 2080 ) * 100, 1 ) per_of_tot_year_hours 3* FROM DUAL ETL_WRK@ORA10GR2>/
PER_OF_TOT_YEAR_HOURS --------------------- 85.8
Cool! Only 86% of my hours in a little over 6 months! Obviously, this is not a good thing. Further breaking the numbers down: VAR C VARCHAR2(10); EXEC :C := '26-AUG-07';
SELECT start_day, end_day, days_between db, SUM( business_days ) bd, ROUND( ( :hours / days_between ), 1 ) hpd, ROUND( ( :hours / ( days_between / 7 ) ), 1 ) hpdw, ROUND( ( :hours / SUM( business_days ) ), 1 ) hpwd, ROUND( ( :hours / SUM( business_days / 5 ) ), 1 ) hpww FROM ( SELECT start_day, end_day, TRUNC( end_day - start_day ) days_between, start_day + rownum dayof, ( CASE WHEN TO_CHAR( start_day + rownum, 'D' ) IN ( 2, 3, 4, 5, 6 ) THEN 1 END ) business_days FROM dual a, ( SELECT TO_DATE( :c, 'DD-MON-YY' ) start_day, TO_DATE( '30-MAR-08', 'DD-MON-YY' ) end_day FROM dual ) b CONNECT BY LEVEL <= TRUNC( end_day - start_day ) ) GROUP BY start_day, end_day, days_between /
START_DAY END_DAY DB BD HPD HPDW HPWD HPWW ---------- ---------- ------ ------ ------ ------ ------ ------ 08/26/2007 03/30/2008 217 155 8.2 57.5 11.5 57.5
DB = Days Between BD = Business Days HPD = Hours Per Day HPDW = Hours Per Day/Week HPWD = Hours Per Work Day HPWW = Hours Per Work Week The scary part is that I am not very diligent about entering my time. It's probably short anywhere between 5 and 10%. I'm not the only one working these kinds of hours either. I know for a fact there are others. Do you think this plays a role in my failed deployments? Labels: discipline, sql, work
DBMS_CRYPTO: Example
Updated 03/08/2010Original source can be found here. Based on comments, specifically from caringaboutsecurity down below, I've (finally) updated the source code. Another point, he is correct that there is no key used in this code. Salt is used, but no key. A key would need to be passed in along with the SSN and stored in some location. This code is not concerned with the overall security aspects, just to encrypt the value. I am not qualified to speak towards the over-arching domain of security. The package specification is also provided in this updated version.
In the comments from my previous post, Tyler and Tom both mentioned the fact that you should never store sensitive data as plain-text in the database. I used a bad example unfortunately, but Tom did mention the DBMS_CRYPTO package which I have used before. Since I have used it and published a working example to the OTN forums, I figured I'd put it here as well. The goal is the encrypt a SSN and store the encrypted string (RAW) in the database. In conjunction with VPD, you should be able to easily control who can see the plain-text version of the SSN. To do that you will need to create another function which decrypts the encrypted SSN. This is a pretty basic example, but it should help to get you started. You can read more about the DBMS_CRYPTO package in the docs. SpecificationCREATE OR REPLACE PACKAGE p_encrypt AS FUNCTION encrypt_ssn( p_ssn IN VARCHAR2 ) RETURN RAW; FUNCTION decrypt_ssn( p_ssn IN RAW ) RETURN VARCHAR2; END p_encrypt; / show errors BodyCREATE OR REPLACE PACKAGE BODY p_encrypt AS --DO NOT FORGET TO WRAP THIS BEFORE LOADING INTO DATABASE --IF IT IS NOT WRAPPED, THE KEY WILL BE EXPOSED --THE WRAP UTILITY IS LOCATED IN THE \BIN DIRECTORY (WRAP.EXE) G_CHARACTER_SET VARCHAR2(10) := 'AL32UTF8'; G_STRING VARCHAR2(32) := '12345678901234567890123456789012'; G_KEY RAW(250) := utl_i18n.string_to_raw ( data => G_STRING, dst_charset => G_CHARACTER_SET ); G_ENCRYPTION_TYPE PLS_INTEGER := dbms_crypto.encrypt_aes256 + dbms_crypto.chain_cbc + dbms_crypto.pad_pkcs5; FUNCTION encrypt_ssn( p_ssn IN VARCHAR2 ) RETURN RAW IS l_ssn RAW(32) := UTL_I18N.STRING_TO_RAW( p_ssn, G_CHARACTER_SET ); l_encrypted RAW(32); BEGIN l_ssn := utl_i18n.string_to_raw ( data => p_ssn, dst_charset => G_CHARACTER_SET );
l_encrypted := dbms_crypto.encrypt ( src => l_ssn, typ => G_ENCRYPTION_TYPE, key => G_KEY ); RETURN l_encrypted; END encrypt_ssn; FUNCTION decrypt_ssn( p_ssn IN RAW ) RETURN VARCHAR2 IS l_decrypted RAW(32); l_decrypted_string VARCHAR2(32); BEGIN l_decrypted := dbms_crypto.decrypt ( src => p_ssn, typ => G_ENCRYPTION_TYPE, key => G_KEY );
l_decrypted_string := utl_i18n.raw_to_char ( data => l_decrypted, src_charset => G_CHARACTER_SET ); RETURN l_decrypted_string; END decrypt_ssn; END p_encrypt; / show errors I mention it in the comments of the code, but do not forget to wrap your PL/SQL before you load it, otherwise someone will be able to easily see your salt/key. Labels: dbms_crypto, howto, oracle, plsql, sql
Failed Deployment...
236 Days 20 Hours 48 Minutes 30 Seconds I hate making mistakes but I've made another one. My streak ends almost 237 days from my previous one. Something so silly too. In our source system, data was been double loaded somehow. So we decided on a surgical delete. A total of 7 DELETE statements needed to be run; 4 on the source system and 3 on the target system. The source system went off without a hitch. I babysat the re-load of the source tables and was ready to have our load jobs run in our target system. What's this? It ran in half the time?! How's that possible? I pulled up our logs to find that zero rows were loaded into one of our tables. There should have been 45 Million plus. I started to run down the possible causes: 1. Did the job we have in the scheduler that TRUNCATEs our persistent staging tables run? Nope. 2. Did I fail to instruct the DBAs correctly in the critical CR? Nope. Instructions look good. 3. Next to the logs, it ran fine on Saturday morning but not Sunday morning. What happened yesterday? 4. Ah yes, my CR. Open up the script...nothing out of the ordinary...and then I saw it. On our target system, we use work tables to pre-generate a keys. It makes things a heck of a lot faster and removes the need for PL/SQL lookups in SQL (no, we don't have incremental builds yet). So the work table needs to be DELETEd from first based on the keys from the first: DELETE FROM some_key_table a WHERE EXISTS ( SELECT NULL FROM the_main_table WHERE business IN ( 'TTT', 'TTR' ) AND dateof = TO_DATE( '24-MAR-08', 'DD-MON-YY' ) AND my_key = a.my_key );
OK, no funny business there. Then I DELETE from the main table: DELETE FROM the_main_table a WHERE EXISTS ( SELECT NULL FROM the_main_table WHERE business IN ( 'TTT', 'TTR' ) AND dateof = TO_DATE( '24-MAR-08', 'DD-MON-YY' ) );
As I look at it I wonder WTH I was thinking using an EXISTS clause on the main table. That's the source. But do you see what I missed? See it yet? OK, I left out the "AND my_key = a.my_key" from the inner query. Obviously a stupid approach, but it would have worked. The best way to do it is to just get rid of the EXISTS clause: DELETE FROM the_main_table a WHERE business IN ( 'TTT', 'TTR' ) AND dateof = TO_DATE( '24-MAR-08', 'DD-MON-YY' ) );
Live and learn, live and learn... Labels: funny, humility, sql, work
Is It Arrogance?
I wrote on Friday night about my experiences that day. I am a very opinionated person. I believe, whole-heartedly, that the database is severely under-utilized, especially at my current employer. I believe that one of the big draws of MySQL is that it's easy for web/application people to pick up. I also believe, in our situation, that's it's a way for application developers to skirt the whole "data" problem. They'll just pawn it off on the Production DBAs to keep the database running. Amusingly, some of our application developers brought down one of our Oracle instances, more than once. Pretty tough thing to do I always thought. I've read articles on bind variables since the beginning, but since it had been drilled into me, I found it quaint. Who would do that? From a C# app someone passed in hundreds of thousands of un-bound INSERT statements. It flooded the shared pool (is that right?) and brought it to a screeching halt. Anyway, back to the point. I've been very vocal lately about MySQL. A few of my friends have begun to warn me that I may be crossing the line towards arrogance. That I will come off as someone resistant to change. I don't see it. But sometimes we're the last to see our own reflection. I don't believe that I am resistant to change. I like change. I just want it to be proven, that's all. I embraced ApEx because it made my life easier. That's all I want. Does this make me arrogant? Labels: database, mysql, oracle, sql, work
MySQL Friday
Each month we have an IT All-Hands meeting. Last month I was promoted to Senior Vice President (SVP), because of my superior management techniques. Today I was promoted to CEO! Unfortunately it only lasted for a few minutes. I happen to resemble our new CEO (and I'm always pining for a promotion) and they thought it would be funny (again) to bring me up. I hugged the guy behind me, shook hands with people next to me and ran up to the front. I wanted to shriek, like the people do on The Price is Right, but I didn't have it in me. You gotta have fun at work right? Well, after that it got serious. Our new Director (at WellCare, Directors are executives, one step up from managers and one below VPs) who heads our architecture team (and release management) got up to discuss where he would be taking us. Slide one: From 3 database engines to 1. From 4 programming languages to 2. From 3 OSs to 1. Wanna guess what question I had? "So, what database engine are we going to use?" I knew the answer, but I take every single opportunity I get to make my point. "MySQL." Being on the datawarehouse team, I was confident that Oracle was not going away. He went on to explain: "Legacy applications would be maintained but everything going forward would be done in MySQL." A flurry of questions came from the crowd so I was unable to followup immediately. I could feel the room come alive...it was weird (I think I'm still hopped up from the events that took place today). Our CIO asked if there were any more questions or comments. I spoke up. I have two points. 1. If it's about cost, move all of the one-off applications into just a few Oracle instances. From what I can tell, we have somewhere in the neighborhood of 100. Let's say 5 databases, datawarehouse, our production OLTP and one for others. All you need to do is assign them different schemas, voila! Cost is much lower and there is a very big chance to reuse code. 2. Actually, I can't remember what my other point was. I think it had something to do with putting the logic in the database, that Java was the fad a few years ago, Ruby was the big thing now, what would it be in 5 years? Will we have to rewrite all of the logic then? (I guess I do sorta remember). After that, someone asked about the two programming languages. Not a great answer from the crowd's reaction. Then someone asked about the OS. The crowd was riotous (if that's a word). The CIO had to calm us all down. I made a remark that he hadn't danced yet (one of our former hazing techniques for new employees) because I didn't want it to be completely personal, or just to ease something that I started. After the meeting, I spoke with the Director. Oracle will be gone in 20 years because of the open source databases, it's being commoditized (not sure what that means). SOA is the wave of the future. It was a polite conversation. I told him I look forward to learning from him but that I will probably never be sold on that idea. Fewer moving parts, simplicity, that's what I want. I then spoke with the CIO, told him that once the decision was made, I would support it and keep my mouth shut (or find a new job). I sent an email to the VP of the Director's group (after a couple of beers...idiot!) explaining my rationale. One of the biggest reasons we chose to come to Tampa, to WellCare specifically, was because it was so young and immature. I would have the opportunity, if I could prove myself, to shape the future of IT here. It's nice to have a voice. Anyway, it's Friday, I'm prepped to spend all weekend at work to get this project delivered that was due in November. Have a good weekend! Labels: cio, database, funny, mysql, oracle, sql, work
SAS: Create SAS Dataset from Oracle
SAS is very prevalent in our environment. When testing, I need to create some datasets for the business users so they they can do their UAT (yes, I shouldn't be involved at this point...but I am). Here's the syntax to do that (I use SAS EG): LIBNAME dmart oracle path=<oracle_sid> schema=<schema_name> pw=<password> user=<db_username>;
CREATE TABLE sasave.mytable AS SELECT * FROM dmart.mytable; QUIT;
You can also use the following syntax: LIBNAME dmart oracle path=<oracle_sid> schema=<schema_name> pw=<password> user=<db_username>;
DATA sasave.mytable_filtered; SET dmart.mytable ( WHERE = id IN ( 5, 6, 7, 8 ) );
RUN;
If you want to use Oracle SQL (SAS guys and gals call it passthrough SQL), here's the syntax for that: PROC SQL NOPRINT;
CONNECT TO oracle( username=<username> pw=<password> path="<oracle_sid>");
CREATE TABLE sasave.dual_test AS SELECT * FROM connection to oracle
( SELECT * FROM DUAL );
disconnect from oracle;
QUIT;
Labels: howto, sas, sql
Instrumentation: DBMS_APPLICATION_INFO
Instrumentation has something that I have come to rely on fairly heavily. I believe I first read about it on asktom, but the one that really spurred me on was this post on instrumentation on his personal blog. Initially, I couldn't really wrap my head around instrumentation. I don't know why it was so difficult; I had a similar problem with sessions when I first started my career. I look back now and it just seems so obvious. Now that I am doing datawarehouse work, nothing is fast. Fast to me is now one hour to load 30 or 40 million records. No more split second queries for me. We currently use no tools. It's straight PL/SQL. Instrumentation of the code is ideal. Actually, it's more instrumentation to aid monitoring. The tool most easily used is provided by Oracle in the DBMS_APPLICATION_INFO package. There are three subprograms that I use most, SET_MODULE, SET_ACTION and most importantly SET_SESSION_LONGOPS. I hadn't started using it until this year, I mainly stuck to the first two. SET_SESSION_LONGOPS is now part of my procedure/function template I've created in JDeveloper. What it allows you to do is set a row in the v$session_longops view (I know it's not actually putting the row in the view...it's the underlying table, but I digress). You can then monitor how your job is doing. Here's an example: dbms_application_info.set_session_longops ( rindex => g_index, slno => g_slno, op_name => 'GETTING MEMBER DATA', sofar => 0, totalwork => l_table.COUNT + 1, target_desc => 'GETTING MEMBER DATA' );
g_index and g_slno are global variables in the package. l_table is a PL/SQL TABLE OF VARCHAR2. Now you can monitor the progress of your job in v$session_longops! Here's the query I use: SELECT username, sid, serial#, TO_CHAR( start_time, 'MM/DD/YYYY HH24:MI:SS' ) start_ti, time_remaining rem, elapsed_seconds ela, ROUND( ( sofar / REPLACE( totalwork, 0, 1 ) ) * 100, 2 ) per, sofar, totalwork work, message, target_desc FROM v$session_longops WHERE start_time >= SYSDATE - 1 ORDER BY start_time DESC
Now you too can sit for hours and watch your job move incrementally forward! But seriously, it does help tremendously to know where a job is at. You can further use the SET_MODULE and SET_ACTION calls to see a specific point in the processing (inside a loop). Here's the code in context: PROCEDURE get_member_data IS l_exists INTEGER; TYPE table_of_lobs IS TABLE OF VARCHAR2(3); l_table TABLE_OF_LOBS := TABLE_OF_LOBS( 'COM', 'ORG' ); l_count INTEGER := 0; BEGIN --check to see if there is enrollment data, if not, move on SELECT COUNT(*) INTO l_exists FROM members WHERE rownum < 2; IF l_exists = 1 THEN--data exists, truncate and reload g_index := dbms_application_info.set_session_longops_nohint; EXECUTE IMMEDIATE 'TRUNCATE TABLE member_stg';
g_audit_key := p_audit.begin_load ( p_targettable => 'MEMBER_STG', p_loadsource => 'MEMBER_SOURCE', p_loadstatus => 'PRE', p_loadprogram => 'GET_MEMBER_DATA', p_commenttext => 'INSERT' );
dbms_application_info.set_session_longops ( rindex => g_index, slno => g_slno, op_name => 'GETTING MEMBERS', sofar => 0, totalwork => l_table.COUNT + 1, target_desc => 'GETTING MEMBERS' );
FOR i IN 1..l_table.COUNT LOOP l_count := l_count + 1;
INSERT INTO member_stg SELECT * FROM members;
g_total_rows_affected := g_total_rows_affected + sql%rowcount;
COMMIT;
dbms_application_info.set_session_longops ( rindex => g_index, slno => g_slno, op_name => 'GETTING MEMBERS', sofar => l_count, totalwork => l_table.COUNT + 1, target_desc => 'GETTING MEMBERS' ); END LOOP;
p_audit.end_load ( p_auditkey => g_audit_key, p_loadstatus => 'SUC', p_rowsuccess => g_total_rows_affected ); gather_table_stats ( p_tablename => 'MEMBER_STG', p_schemaname => 'MYHOME' );
dbms_application_info.set_session_longops ( rindex => g_index, slno => g_slno, op_name => 'GETTING MEMBERS', sofar => l_count + 1, totalwork => l_table.COUNT + 1, target_desc => 'GETTING MEMBERS' );
END IF;
EXCEPTION WHEN others THEN p_audit.failed_load ( p_auditkey => g_audit_key, p_comments => SQLCODE || ' ' || SQLERRM ); RAISE;
END get_member_data;
Labels: dbms_application_info, development, instrumentation, plsql, sql, work
|