I have table (call it my_table) that can be simplified like this: NAME, SEQ_NO, LOCKED.
Items get removed and added and I want to reorder them (modify SEQ_NO) in a way that the sequence always goes from 1 to COUNT(*) and items that are locked retain their SEQ_NO and no unlocked item would get that number. Only unlocked items are updated with new SEQ_NO.
Example:
This
NAME SEQ_NO LOCKED Foo 1 N Bar 3 Y Abc 4 Y Baz 5 N Cde 7 N
would result in:
NAME SEQ_NO LOCKED Foo 1 N Baz 2 N Bar 3 Y Abc 4 Y Cde 5 N
How could I do that?
There are times when your goals of
1..COUNT(*)numbering and ‘do not renumber locked rows’ lead to irresolvable conflict. For example:I will assume that the required output for this scenario is:
You example shows the unlocked data being kept in its original sequence number order, and the locked data obviously doesn’t get a new number.
I assume that there are no duplicate sequence numbers in the original data.
Quick Summary
It is an interesting and tricky problem. The key to reordering the data is knowing where to place the unlocked rows. In the example data:
We can give the unlocked rows a sequence number counting from 1..3, so we end up with pairs of ord:old sequence A { 1:1, 2:5, 3:7 }. We can generate a list of slots for the result set 1..5. We remove from that list of slots those slots held by locked rows, leaving { 1, 2, 5 } as the list of slots to be occupied by unlocked rows in the reordered list. We then number those in order too, leaving pairs ord:new B { 1:1, 2:2, 3:5 }. We can then join these two lists, A and B, on the first field and project away the sequencing, to leave pairs of new:old slot numbers C { 1:1, 2:5, 5:7 }. The locked rows produce a set of new:old values where new = old in each case, so D { 3:3, 4:4 }. The final result is the union of C and D, so the result set contains:
This works for the case where the locked rows have sequence number 13 and 14 too; the unlocked rows are allocated new sequence numbers 1, 2, 3 and the locked rows remain unchanged. One of the comments to the question asks about ‘1 locked, 5 unlocked, 10 locked’; this would produce ‘1 locked, 2 unlocked, 10 locked’.
Getting to do that in SQL takes quite a lot of SQL. Someone with a good command of the OLAP functionality might be able to get there quicker than my code does. And converting the SELECT results into an UPDATE statement is tricky too (and not fully solved by me). But being able to get the data presented in the correct result order is crucial, and the key to solving that is the ordering steps represented by lists A and B.
TDQD — Test Driven Query Design
As with any complex SQL query operation, the secret is to build the query up step by step. As noted, we need to treat locked and unlocked rows differently. In this case, the target is ultimately an UPDATE statement, but we need to know how to generate the data for the UPDATE, so we do the SELECT first.
Renumberable rows
When appropriate, these can be ordered with an ORDER BY clause, but sub-queries typically don’t allow ORDER BY clauses and we need to generate a number. With OLAP functions, you can probably do this more compactly. In Oracle, you may be able to use ROWNUM to generate row numbers. There’s a trick that will work in any DBMS, though it is not particularly fast.
Renumbered rows assuming no interference from locked rows
This is a non-equijoin and that is what makes this a not particularly fast operation.
Unrenumberable rows
New sequence numbers
Suppose we manage to get a list of numbers, 1..N (where N = 5 in the sample data). We remove from that list the locked entries (3, 4) leaving (1, 2, 5). When those are ranked (1 = 1, 2 = 2, 3 = 5), we can join the ranking with the unlocked records new sequence, but use the other number as the final sequence number of the record. That just leaves us with a few little problems to resolve. First, generating each of the numbers 1..N; we can do one of those ghastly little non-equijoin tricks, but there should be a better way:
We can then remove the locked sequence numbers from this list:
Now we need to rank those, which means another of the self-joins, but this time on that expression. Time to use ‘Common Table Expressions’ or CTE, also known as a ‘WITH clause’:
Finishing up
So, now we need to join that result with Query 2 to get the final numbers for the unlocked rows, and then the union of that with Query 3 to get the required output. Of course, we have to get the correct values for Locked in the output too. Still going step-wise:
This needs to be combined with a variant of Query 3:
The result set
Combining these yields:
This gives the result:
So, it is possible (though far from easy) to write a SELECT statement that orders the data correctly.
Converting into an UPDATE operation
Now we have to find a way to get that monstrosity into an UPDATE statement. Left to my own devices, I’d think in terms of a transaction that selects the result of Query 8 into a temporary table, then deletes all the records from the source table (
My_Table) and inserts the appropriate project of the result of Query 8 into the original table and then commits.Oracle doesn’t seem to support dynamically created ‘per session’ temporary tables; only global temporary tables. And there are sound reasons not to use those, for all they are SQL Standard. Nevertheless, it will do the trick here where I’m not sure what else will work:
Separately from this work:
Then:
You probably can do it with an appropriate UPDATE; you need to do some of the thinking.
Summary
It isn’t easy, but it can be done.
The key step (at least for me) was the result set from Query 6, which worked out the new positions of the unlocked rows in the updated result set. That is not immediately obvious, but it is crucial to producing the answer.
The rest is simply support code wrapped around that key step.
As noted previously, there are likely to be many ways to improve some of the queries. For example, generating the sequence
1..Nfrom the table might be as simple asSELECT ROWNUM FROM My_Table, which compacts the query (highly beneficial — it is verbose). There are OLAP functions; one or more of those may be able to help with the ranking operations (probably more concisely; like performing better too).So, this is not a polished final answer; but it is a strong push in the right general direction.
PoC Testing
The code has been tested against Informix. I had to use somewhat different notations because Informix does not (yet) support CTEs. It does have very convenient, very simple, per session dynamic temporary tables introduced by
INTO TEMP <temp-table-name>which appears where the ORDER BY clause might otherwise appear. Thus, I simulated Query 8a with: