I need to find the next available ID on a table that has keys that are strings. I have followed an example here. My example below:
Dishes Table (first columns)
Table_id
DSH0000000003
DSH0000000004
DSH0000000005
DSH0000000007
SQL:
SELECT CONCAT('DSH',LPAD(MIN(SUBSTRING(t1.dish_id FROM 4) + 1), 10, '0')) AS nextID
FROM dishes t1
WHERE NOT EXISTS (SELECT t2.dish_id
FROM dishes t2
WHERE SUBSTRING(t2.dish_id FROM 4) = SUBSTRING(t1.dish_id FROM 4) + 1)
Output:
DSH0000000006
If I delete #5 then it returns #5 but it does not return "DSH0000000001".
You may use variables to build consecutive ids, then compare where's first non-matched id:
SELECT
CONCAT('DSH', LPAD(seq, 10, '0')) AS k
FROM
(SELECT
#seq:=#seq+1 AS seq,
num
FROM
(SELECT
CAST(SUBSTR(table_id, 4) AS UNSIGNED) AS num
FROM
t
UNION ALL
SELECT
MAX(CAST(SUBSTR(table_id, 4) AS UNSIGNED))+2 AS num
FROM
t
ORDER BY
num) AS ids
CROSS JOIN
(SELECT #seq:=0) AS init
) AS pairs
WHERE
seq!=num
LIMIT 1
Fiddle is available here.
You should add a test of your start value. With your current approach you can only get values greater than the minimum value that exists in your table.
You could use IF() to distinguish the two possibilities.
Related
I am looking for the most efficient way to find the next or previous ID of the following query:
SELECT *
FROM transactions
ORDER
BY CASE order_status
WHEN 'order_accepted' THEN 1
WHEN 'processing_order' THEN 2
WHEN 'order_send_mailer' THEN 3
WHEN 'order_send' THEN 4
WHEN 'order_received' THEN 5
WHEN 'order_refunded' THEN 6
ELSE 7 END
, id DESC limit 1;
I tried adding a where id > '$id' or where id < '$id' claus to the query but it didn't give me te next or previous ID I was looking for.
For those that need some explanation of what I am trying to do: It's to go to the next or previous order by case with a forward of backward button.
What it currently looks like:
-id- -order_status-
9399 order_accepted
9398 processing_order
9363 processing_order
9403 order_send_mailer
9318 order_send
9346 order_received
9345 order_received
9050 order_refunded
The next ID for example of 9403 would be 9363 and previous ID would be 9318
Change your order_status into an enum column. This will save disk space and make sorting by order_status simpler and faster.
-- Add a new version of the column using an enum.
-- These strings are aliases for ordered numbers.
-- 'order_accepted' is 1, 'processing_order' is 2, etc.
alter table transactions add column enum_order_status enum(
'order_accepted',
'processing_order',
'order_send_mailer',
'order_send',
'order_received',
'order_refunded'
) not null;
-- Copy the status into the new enum column.
-- MySQL will translate the string into the number for you.
update transactions
set enum_order_status = order_status;
-- Drop the old column.
alter table transactions drop column order_status;
-- Rename the new enum column.
alter table transactions rename column enum_order_status to order_status;
-- Index it.
create index transactions_order_status on transactions(order_status);
-- Enjoy your vastly simplified and much faster query.
select *
from transactions
order by order_status, id desc
That's not actually necessary, but it makes everything much simpler.
With that out of the way, use the window functions lead and lag to refer to the previous and next rows in a query.
select
id, order_status,
lead(id) over w, lead(order_status) over w,
lag(id) over w, lag(order_status) over w
from transactions
window w as (order by order_status, id desc);
Note, window functions were added in MySQL 8. If you're using an older version I recommend upgrading ASAP; MySQL 8 has many big improvements. Otherwise you can simulate it with correlated subqueries and self-joins.
If you want the previous and next rows of a specific row, use the technique from this answer. We add row_numbers to the table in the desired order, and then fetch 9403 and its previous and next row by row number.
-- Add a row number to your table in the desired order.
with ordered_transactions as (
select
*, row_number() over w as rn
from transactions
window w as (order by order_status, id desc)
)
select *
from ordered_transactions
-- Find the row number for ID 9403, then add -1, 0, and 1.
-- If 9403 is row number 5 you'll fetch row numbers 4, 5, and 6.
where ot.rn in (
select rn+i
from ordered_transactions ot
-- All this is doing is making us three "rows" where i = -1, 0, and 1.
cross join (SELECT -1 AS i UNION ALL SELECT 0 UNION ALL SELECT 1) cj
where ot.id = 9403
);
Try it.
I need to generate unique "ids", the catch is, it can be only between 1 - 99999.
The "good" thing is, that it has to be unique only in group with another column.
We have groups, each group has its own "group_id" and each group need something like unique('group_id', 'increment_id')
The 99999 records is enough for several years for each group right now, but it is not enough for all groups together, therefore I cant just create table with AUTO_INCREMENT and inserting there records and taking its auto increment.
For example, if I need 5 records for Group one and three records for Group two, I suppose to get something like this:
group_id, increment_id
1, 1,
1, 2,
1, 3,
1, 4,
1, 5,
2, 1
2, 2,
2, 3
Also, the conflict is not an option, therefore using something like "length" can be tricky, if done programatically (there can be i.e. 10 requests at once, each of them first select length for given group_id and then tries to create 10 rows with same increment_id)
However I am thinking - if I set it up as the value of subselect of count, than it will always be "ok"?
You can create a auxiliar table named counters to manage that:
table: counters
columns: group_id, current_counter
OR
Each time you insert a row increment_id = select max(increment_id)+1 from table_xxx where group_id = group_xxxx
You can use user variables to get the incrementing number within each group_id:
select
t.*,
#rn := if(#group_id = group_id,
#rn + 1,
if(#group_id := group_id, 1, 1)
) increment_id
from (
select group_id
from your_table t
/* some where clauses */
order by group_id
) t
cross join (
select #rn := 0,
#group_id := - 1
) t2
I'm currently using this way for selecting before and after of selected row:
SELECT rank, id FROM (
SELECT
id,
#i:=#i + 1 rank,
#match:=IF(table.id = 50, #i, #match)
FROM
table t,(SELECT #i:=0, #match:=0) vars
ORDER BY t.t_date DESC
) t2
WHERE
#match>=rank-1 AND #match<=rank+1;
As you can see I selected equal, before and after rows id=50.
Question: I'm creating match row with IF condition and place it inside var. Is it this way memory and performance efficient?
MariaDB 10.2 implements a lot of Window Functions, and does it efficiently.
Reference
I have a table which contains a column called ticket_id and it contains values as follows:
ticket_id
STK0000000001
STK0000000002
STK0000000001
STK0000000003
STK0000000002
STK0000000001
The ticket_id value will repeat in certain rows, so it is not unique.
I am using this query to get the next available id, but I am not able to get it working. It always returns STK0000000002.
Any help is appreciated!
SQL:
SELECT
CONCAT('STK', LPAD(seq, 10, '0')) AS nextID
FROM
(SELECT
#seq:=#seq+1 AS seq,
num
FROM
(SELECT
CAST(SUBSTR(ticket_id, 4) AS UNSIGNED) AS num
FROM
sma_support_tickets
UNION ALL
SELECT
MAX(CAST(SUBSTR(ticket_id, 4) AS UNSIGNED))+2 AS num
FROM
sma_support_tickets
ORDER BY
num) AS ids
CROSS JOIN
(SELECT #seq:=0) AS init
) AS pairs
WHERE
seq!=num
LIMIT 1
Maybe I'm missing something in your question, but it seems that this should do it:
SELECT CONCAT('STK',
LPAD(MAX(SUBSTRING(ticket_id, 4)) + 1,
10,
'0')
)
FROM sma_support_tickets;
Try: This table must have one serial number or unique number or ID for the table for each row. Find out that unique number(primary key) through code and add 1 or increment that number, but not to the ticket_id as you are doing it now. so that it can move forward to next row.
The issue here is related to another question I had...
I have millions of records, and the ID of each of those records is auto-incremented, unfortunately sometimes the ID that is generated is sometimes thrown away so there are many many gaps between IDs.
I want to find the gaps, and re-use the ids that were abandoned.
What's an efficient way to do this in MySQL?
First of all, what advantage are you trying to get by reusing the skipped values? An ordinary INT UNSIGNED will let you count up to 4,294,967,295. With "millions of records" your database would have to grow a thousand times over before running out of valid IDs. (And then using a BIGINT UNSIGNED will bump you up to 18,446,744,073,709,551,615 values.)
Trying to recycle values MySQL has skipped is likely to use up a lot of your time trying to compensate for something that really doesn't bother MySQL in the first place.
With that said, you can find missing IDs with something like:
SELECT id + 1
FROM the_table
WHERE NOT EXISTS (SELECT 1 FROM the_table t2 WHERE t2.id = the_table.id + 1);
This will find only the first missing number in each sequence (e.g., if you have {1, 2, 3, 8, 10} it will find {4,9}) but it's likely to be efficient, and of course once you've filled in an ID you can always run it again.
The following will return a row for each gap in the integer field "n" in mytab:
/* cs will contain 1 row for each contiguous sequence of integers in mytab.n
and will have the start of that chain.
ce will contain the end of that chain */
create temporary table cs (row int auto_increment primary key, n int);
create temporary table ce like cs;
insert into cs (n) select n from mytab where n-1 not in (select n from mytab) order by n;
insert into ce (n) select n from mytab where n+1 not in (select n from mytab) order by n;
select ce.n + 1 as bgap, cs.n - 1 as egap
from cs, ce where cs.row = ce.row + 1;
If instead of the gaps you want the contiguous chains then the final select should be:
select cs.n as bchain, ce.n as echain from cs,ce where cs.row=ce.row;
This solution is better, in case you need to include the first element as 1:
SELECT
1 AS gap_start,
MIN(e.id) - 1 AS gap_end
FROM
factura_entrada e
WHERE
NOT EXISTS(
SELECT
1
FROM
factura_entrada
WHERE
id = 1
)
LIMIT 1
UNION
SELECT
a.id + 1 AS gap_start,
MIN(b.id)- 1 AS gap_end
FROM
factura_entrada AS a,
factura_entrada AS b
WHERE
a.id < b.id
GROUP BY
a.id
HAVING
gap_start < MIN(b.id);
If you are using an MariaDB you have a faster option
SELECT * FROM seq_1_to_50000 where seq not in (select col from table);
docs: https://mariadb.com/kb/en/mariadb/sequence/