How to unnest an integer array represented as a BLOB? - sqlite

SQLite doesn’t have native support for arrays. I would think that my method (thinking outlined below) of making a custom BLOB encoding would be a fairly common workaround (yes, I need an array rather than normalizing the table). The benefit of representing an integer array as a BLOB is primarily the space savings, for example:
13,24,455,23,64789
Stored as TEXT will take up 18 bytes (commas included, making assumptions here). But if one were to store the above TEXT in a custom encoded BLOB format it would look like this:
0x000D001801C7FD15
Where every number is assumed to take up 2 Bytes (0 to 65,535 or 0000 to FFFF). This BLOB, to my understanding, would then be 10 Bytes. This is nearly half the size as storing it in a delimited TEXT format. This space savings would also be magnified by the number of rows and the number of integers in the array.
Is there a good way of unnesting a BLOB by width? Say that I want to unnest the BLOB so that each row represents an integer. Can I take the above BLOB and turn it into this?
id
number
1
000D
2
0018
3
01C7
4
FD15

SQLite's HEX() function
interprets its argument as a BLOB and returns a string which is the
upper-case hexadecimal rendering of the content of that blob.
After you get the blob as a string use SUBSTR() to slice it in 4 char parts:
WITH cte(id) AS (VALUES (1), (2), (3), (4), (5))
SELECT HEX(t.col) hex_col,
c.id,
SUBSTR(HEX(t.col), (c.id - 1) * 4 + 1, 4) number
FROM tablename t JOIN cte c
ORDER BY t.rowid, c.id;
Or, with a recursive CTE to generate dynamically the list of values 1..n:
WITH cte(id) AS (
SELECT 1
UNION ALL
SELECT id + 1
FROM cte
WHERE id < 5
)
SELECT HEX(t.col) hex_col,
c.id,
SUBSTR(HEX(t.col), (c.id - 1) * 4 + 1, 4) number
FROM tablename t JOIN cte c
ORDER BY t.rowid, c.id;
Change col to the name of your blob column.
See the demo.

Related

SQLite3 - Calculated SELECT with padding and concatenation

I have the following SQLite table (a stub of the real table which has a few other columns)
CREATE TABLE IF NOT EXISTS fingers(id INTEGER,intLL INTEGER,fracLat INTEGER,fracLng INTEGER,PRIMARY KEY(id)) WITHOUT ROWID;
A typical entry in this table would be along the lines of
INSERT INTO fingers(id,intLL,fracLat,fracLng) VALUES(1,12899,42513,4025);
From time-to-time I need to query this table to pull out rows for matching intLL values in such a way that a calculated value meets a variable condition. For example
SELECT * FROM fingers WHERE intLL = 12899 AND ('8508' = (CAST((ROUND(CAST(fracLat AS REAL)/500))
AS INTEGER) || CAST((ROUND(CAST(fraCLng AS REAL)/500)) AS INTEGER)));
Explanation
Transform the fractLat and fracLng columns by dividing them by 10,250 or 500. The CAST AS REAL is required to prevent the default integer division that would be performed by SQLite
Round the decimal result to the closest integer. After rounding you will by default get a value with a trailing .0. The CAST AS INTEGER ensures that this is removed
Concatenate the two parts. The concatenation is going wrong. In the present case the concatenated result would be 858 which is not what I want
Compare against an incoming value: 8508 in this case.
My questions
How can I pad the two parts with 0s when required prior to concatenation so as to ensure that they have the same number of digits
Is there a simpler way of achieving this?
One way to pad 0s is to concatenate 00 at the start of the number and with SUBSTR() return the last 2 chars.
Also, you can divide by 500.0 to avoid integer division:
SELECT * FROM fingers
WHERE intLL = 12899
AND '8508' = SUBSTR('00' || CAST(fracLat / 500.0 AS INTEGER), -2) ||
SUBSTR('00' || CAST(fraCLng / 500.0 AS INTEGER), -2)
Another way to do it is with the function printf() which formats a number:
SELECT * FROM fingers
WHERE intLL = 12899
AND '8508' = printf('%02d', fracLat / 500.0) ||
printf('%02d', fraCLng / 500.0)
See the demo.

Split column value in sqlite

Am new to sqlite in my learning I come across the subString function so in my exercise, My table name is t1 and my column value is Partha000099 I want to increment by 1 eg., Partha000100 when i try with
SELECT SUBSTR(MAX(ID),6) FROM t1
am getting output as 000099 when I increment by 1 with the below query
SELECT SUBSTR(MAX(ID),6)+1 FROM t1
am getting output as 100, Now my question is how to construct it back as I expect
I tried with the below query,
SELECT 'Partha' || SUBSTR(MAX(ID),6)+1 FROM t1
am getting output as 1. Please some one help me.
While my solution will work, I would advice you against this type of key generation. "SELECT MAX(ID)+1" to generate the next key will be fraught with problems in more concurrent databases and you risk generating duplicate keys in a busy application/system.
It would be better to split the key into two columns, one with the group or name 'Partha', and the other column with an automatically incremented number.
However, having said that, here's how to generate the next key like your example.
You need to:
Split the key into two
Increment the numeric part
Convert it back to a string
Pad it to 6 digits
Here's the SQL that will do that:
SELECT SUBSTR(ID, 1, 6) || SUBSTR('000000' || (SUBSTR(MAX(ID), 7)+1), -6) FROM t1;
To pad it to 6 digits, I prepend 6 zeroes, then grab the last 6 digits from the resulting string with this type of expression
SUBSTR(x, -6)
The reason why you got 1 was that your expression was grouped like this:
SELECT .... + 1
And the .... part, your string concatenation, was then attempted converted to a number, which resulted in 0, thus 0+1 gives 1.
To get the unpadded result you could've just added some parenthesis:
SELECT 'Partha' || (SUBSTR(MAX(ID),6)+1) FROM t1
^ ^
This, however, would also be wrong as it would return Partha1, and that is because SUBSTR(..., 6) grabs the 6th character and onwards and the 6th character is the final a in Partha, so to get Partha100 you would need this:
SELECT 'Partha' || (SUBSTR(MAX(ID),7)+1) FROM t1
^

Find range of values between 2 columns in Oracle DB

Hi I have a table with 2 columns with range, so for e.g If Range Start = ABC1/000/0/0000 and Range END = ABC1/000/0/1022 .
I have to get all the values between this range and then join this with another table. Can you let me know how can I get all the values in DUAL table. I am using Oracle 11g.
Basically I need to make a list with first value as ABC1/000/0/0000 second as ABC1/000/0/0001 till ABC1/000/0/1022.
I have no idea what you mean by "storing values temporarily in DUAL". DUAL is a singe column table with a single value!
However, something like this might be what you want. If its not, then perhaps you could elaborate on your problem a little further
select blah
from another_table
where somekey in
( select blah
from table
where col between <rangeStart> and <rangeEnd>
)
So, it seems you need a few things.
Separate the "last value" from a slash-separated string, such as
ABC1/000/0/0000. It is best to do this with standard substr() and
instr() functions, not with regular expressions (for faster
execution). In instr() we can use a negative argument for
occurrence, to indicate "counting from the end of the string".
Something like this:
select range_from, substr(range_from, instr(range_from, '/', -1) + 1
from ...
Actually, you will need to convert this to a number with to_number() for further processing, and you will also need to capture the substring up to the last slash (similar use of substr() and instr(). And you will need to do the same for range_to.
Generate all the numbers from the first value to the last value. This is easily done with a connect by level query (hierarchical query). Some care must be taken since we may need to do this for several input rows (input ranges) at once.
Then put everything back together and use the result in further processing.
I will assume that the range_from string contains at least one slash, that the substring between the last slash and the end of the string represents a non-negative integer in character format, and the range_to similarly contains at least one slash and the substring from the last slash to the end of the string represents a non-negative integer. It is your responsibility to guarantee that this integer is greater than or equal to the one from range_from. In the output I will use the same substring UP TO the last slash as I find in range_from; if the requirement is that range_to must have the same initial substring, it is your responsibility to guarantee that.
I will also assume that the width (number of characters) of the "number" part (the last token in the strings) is not known beforehand and must be calculated in the query.
with
test_data( id, range_from, range_to ) as (
select 1, 'ABC1/000/0/2033', 'ABC1/000/0/2035' from dual union all
select 2, 'xyz/33/200' , 'xyz/33/200' from dual union all
select 3, '300/LMN/000' , '300/LMN/003' from dual
)
-- end of test data; SQL query begins below this line
select id, stub || lpad(to_char(from_nbr + level - 1), len, '0') as val
from (
select id, stub, length(from_str) as len, to_number(from_str) as from_nbr,
to_number(to_str) as to_nbr
from (
select id, substr(range_from, 1, instr(range_from, '/', -1)) as stub,
substr(range_from, instr(range_from, '/', -1) + 1) as from_str,
substr(range_to , instr(range_to , '/', -1) + 1) as to_str
from test_data
)
)
connect by level <= 1 + to_nbr - from_nbr
and prior id = id
and prior sys_guid() is not null
order by id, level -- if needed
;
ID VAL
-- --------------------
1 ABC1/000/0/2033
1 ABC1/000/0/2034
1 ABC1/000/0/2035
2 xyz/33/200
3 300/LMN/000
3 300/LMN/001
3 300/LMN/002
3 300/LMN/003

SQLite: How to reduce byte size of integer values?

I have a SQLite table (without row ID, but that's probably irrelevant, and without any indexes) where my rows contain the following data:
2 real values, one of which is the primary key
3 integers < 100
1 more field for integers, but currently always null
According to http://www.sqlite.org/datatype3.html, integer values can take 1, 2, 3, 4, 6 or 8 bytes according to their magnitude. Therefore I'd expect each row in my table to take up about 20 bytes. In reality, sqlite3_analyzer gives me for the table
Average payload per entry......................... 25.65
Maximum payload per entry......................... 26
which is somewhere in between the minimum value of 20 and the maximum of 32 (if all integers were stored with 4 bytes). Is it possible to give the DB a "hint" to use even smaller integer types wherever possible? Or how else can the discrepancy be explained? (I don't think it's indexes because there are none for this table.)
Similarly, on a previous table I had 2 real values + 2 small integers and each entry occupied slightly more than 24 bytes (which is also more than I would have expected).
Also, there is no way to store floats in single precision with SQLite right?
The actual record format has one integer for the header size, one integer for each column to describe the value's type, and all the data of the column values.
In this case, we have:
bytes
1 header size
6 column types
16 two real values
3 three small integers between 2 and 127
0 NULL
--
26

Is it possible to order by a manual list

I am trying to create a query which groups payments into ranges (e.g. 4-, 5 - 9, 10 - 49, 50 - 99, 100 - 149, 150+).
If I try to order these by the above range they appear in alphabetical order (as you would expect).
Is it possible for me to order these by a manual list (see above range)
What's your TD release?
TD14 supports regular expressions, simply extract the first string of digits and cast it to an integer:
ORDER BY CAST(REGEXP_SUBSTR(grp, '[0-9]+') AS INTEGER)
You can use OTRANSLATE, which basically allows you to specify characters and replace them with another.
EDIT : Thanks to JNeville for setting me straight on this being ranges. The same idea still applies though, if you take his suggestion to make the last entry a range as well.
So, assuming you only have numbers, +, and -, and white space:
select
otranslate(<your column>,'+- ','')
from
<your table>
Which should return just the numeric portion of those strings. Then you should be able to cast it as an integer, and sort it.
create volatile table vt as
(select cast ('-5' as varchar(10)) as theCol)
with data
on commit preserve rows;
INSERT into vt values ('10 - 49');
INSERT INTO vt values ('50 - 99');
insert into vt
values ('150-9999');
select
cast (otranslate(theCol,'+- ','') as integer) as theNum
from
vt
order by theNum
5
1049
5099
1509999

Resources