I need to make a point in polygon MySQL query.
I already found these two great solutions:
http://forums.mysql.com/read.php?23,286574,286574
MySQL implementation of ray-casting Algorithm?
But these functions can only check if one point is inside a poly. I have a query where the PiP part should only be one part of the query and check for x points inside a polygon.
Something like this:
$points = list/array/whatever of points in language of favour
SELECT d.name
FROM data AS d
WHERE d.name LIKE %red%
// just bla bla
// how to do this ?
AND $points INSIDE d.polygon
AND myWithin( $points, d.polygo ) // or
UPDATE
I tried it with MBR functions like this:
SET #g1 = GeomFromText('Polygon((13.43971 52.55757,13.41293 52.49825,13.53378 52.49574, 13.43971 52.55757))');
SET #g2 = GeomFromText('Point(13.497834 52.540489)');
SELECT MBRContains(#g1,#g2);
G2 should NOT be inside G1 but MBR says it is.
So really your question is how do you apply the same function to multiple values and return true only if all the call to the function return true. That's not very hard.
If it were me I'd put the points (and optionally polygons - not shown in example) into a table then write a MySQL function to apply the raycasting method to each point - returning false if any one interation returns false, then return true. In the example below I've assumed that the polygon is fetched from yourpolygon and identified by primary key, while points are indentified by a foreign key (using the function mywithin by zarun) :
DECLARE FUNCTION allwithin(
pointSetKey INT)
RETURNS INT(1)
BEGIN
DECLARE current POINT;
DECLARE check CURSOR FOR SELECT p.point
FROM yourpoints p
WHERE p.set=pointSetKey;
OPEN check;
nextPoint: LOOP
FETCH check INTO current;
IF (0=mywithin(current, yourpolygon)) THEN
RETURN 0;
END IF;
END LOOP;
RETURN 1;
END;
INSERT INTO yourpoints (pointsetkey, point)
VALUES (
128,
GeomFromText('Point(13.497834 52.540489)')
),
(
128,
GeomFromText('Point(13.6 52.1)')
),
....
;
SELECT allwithin(128
, GeomFromText('Polygon((13.43971 52.55757,13.41293 52.49825,13.53378 52.49574, 13.43971 52.55757))')
);
or...
SELECT COUNT(*)
FROM yourpoints p
WHERE p.set=128
AND mywithin(p.point
, GeomFromText('Polygon((13.43971 52.55757,13.41293 52.49825,13.53378 52.49574, 13.43971 52.55757))')
);
Will give you the number of points not inside the polygon (which is rather expensive when you only want to know if NONE of the points are outside).
what if you do
SET #g1 = GEOMFROMTEXT('Polygon((13.43971 52.55757,13.41293 52.49825,13.53378 52.49574, 13.43971 52.55757))');
SET #g2 = GEOMFROMTEXT('Point(13.497834 52.540489)');
SELECT ST_Contains(#g1,#g2);
instead of MBRContains? As I understand MySQL spatial documentation. MBR* functions are Minimum-bounding-rectangle ones, so it shows you whether your point is within the minimum rectangle over your geometry, but not in the geometry itself (in case it is irregular polygon and the point is inside the MBR and not in the polygon)
It seems to me as follows: you need to test whether multiple points are in a polygon. If all are, then so is their convex hull. Figure out their convex hull (basically: order them clock-wise or anti-clockwise), this creates a polygon. Now test whether the new polygon is inside d.polygon.
Related
I am trying to filter a set of appartments by computing every possible surface the building has got, and then checking if it matches my criterias or not.
Example : I have one building A that is composed of 3 appartments of 200m² each, let's name them 1, 2, and 3.
In my search, I should be able to retrieve that building if my criterias meet these given cases :
I'm looking for 200m² (We have three appartments that match this criteria)
I'm looking for 400m² (We have a few possible SUMS of surface in the building that would match, whether it's 1+2, 1+3, 2+3 doesn't matter)
I'm looking for 600m² (We have the SUM of all surfaces of the building, 1+2+3)
I am able to answer to the first case with a MIN(), so I get the smallest surface available. I am also able to answer to the last case because I get the max available surface possible with a SUM() of all appartments.
But the second case is troubling me, I don't know if i can compute these "Possible SUMS", as I'd call them, inside a query.
Here's the SQL I've got so far, knowing well that it doesn't answer the second case :
SELECT DISTINCT building.*
FROM building AS building
LEFT JOIN appartment a ON a.id_building = building.id
WHERE (
SELECT MIN(a2.surface)
FROM appartment a2
INNER JOIN building ON a2.id_building= building.id
) >= 399
AND (
SELECT SUM(a2.surface)
FROM appartment a2
INNER JOIN building ON lot.id_building= building.id
) <= 401
I tried to work with the whole set of results with PHP rather than in SQL but it's not my prefered option because it would mean the redoing of a lot of work that hasn't been done by me, so it quickly got harder. I also read about HAVING statements but I don't understand where I should put them and what the condition inside should be.
maybe something like this, but i'm not very sure about your table structure
SET #totalSurface= 0;
SELECT #totalSurface:=#totalSurface+`surface`, `id`, `building` FROM `apartment`
WHERE #totalSurface=400
GROUP BY `building` ;
I finally managed to find something using a stored function. I use this cursor with the building id as parameter to loop through every appartement of said building, sorted by surface. Then, for each loop, i add the surface to my total, and check if I am or not in my interval of given criterias. If yes, set return value to true, if not, return value stays false.
Here it goes, I hope it helps if someone is stuck as I was :
CREATE DEFINER=`homestead`#`%` FUNCTION `buildingHasSurface`(`surface_min` INT, `surface_max` INT, `building_id` INT) RETURNS TINYINT(1)
BEGIN
DECLARE curseurFini INT;
DECLARE valide INT;
DECLARE surface_totale INT;
DECLARE surface_row INT;
DECLARE curs CURSOR FOR SELECT surface_dispo FROM appartement WHERE id_building = building_id ORDER BY surface_dispo ASC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET curseurFini = 1;
OPEN curs;
SET curseurFini = 0;
SET surface_totale = 0;
SET valide = 0;
REPEAT
FETCH curs INTO surface_row;
IF curseurFini = 0 THEN
SET surface_totale = surface_totale + surface_row;
IF surface_totale >= surface_min
AND surface_totale <= surface_max
THEN
SET valide = 1;
END IF;
END IF;
UNTIL curseurFini END REPEAT;
CLOSE curs;
RETURN valide;
END$$
I tried to create mysql function. For particular month it returning empty result. I need to set that empty result as '0'. How to do this? Here is my mysql function.
DELIMITER $$
CREATE FUNCTION `monthtargetbyuser`(`userId` BIGINT,month VARCHAR(11),year VARCHAR(11)) RETURNS int(11)
BEGIN
DECLARE target,da BIGINT;
set da = year-month;
SET target = (SELECT ifnull(user_target.monthly_target,0) as monthly_target from user_target left join users on users.id=user_target.user_id where date_format(users.doj,'%Y-%m') <= 'da' and user_target.year=year and year(users.doj)>0 and users.locked !=1 and users.id =userId );
RETURN target;
END
Thanks in Advance!!
set da = year-month; -> year and month are declared as varchars, however, in this context they would be interpreted as integers. If you wanted to create a string such as '2016-05', then use the concat() function: concat(year,'-',month). I have absolutely no idea why you declared da as bigint. It should be declared as varchar.
date_format(users.doj,'%Y-%m') <= 'da' -> this expression compares a string in year-month format with the string 'da'. Sql is not php, the variable names will not be expanded within a string. So, the expression should be: date_format(users.doj,'%Y-%m') <= da.
I also believe that you mixed up the order of the tables in the join. You should not really have a target for a user that does not exist.
If there is no such record that would satisfy your query, then the ifnull() function in the select statement will not even run, this is why you get an empty result in certain occasions. (Although how you tested your function is beyond me, given the issues above). Apparently, you expect to have a maximum of one record returned. In this case I suggest to use max() or min() function within the ifnull() because they guarantee to return a value: SELECT ifnull(max(user_target.monthly_target),0) as monthly_target ...
I found a solution... I made a mistake in concat of year and month. That makes a issue. This is my corrected code.
DELIMITER $$
CREATE FUNCTION `monthtargetbyuser`(`userId` BIGINT,month int(11),year int(11)) RETURNS BIGINT
BEGIN
DECLARE target,da varchar(50);
set da =concat(year,'-',month);
SET target = (SELECT ifnull(min(user_target.monthly_target),0) as monthly_target from user_target left join users on users.id=user_target.user_id where date_format(users.doj,'%Y-%m') <= da and user_target.year=year and year(users.doj)>0 and users.locked !=1 and users.id =userId );
RETURN target;
END
Thanks a lot. #Shadow
Getting the Value:
I've got the levenshtein_ratio function, from here, queued up in my MySQL database. I run it in the following way:
$stmt = $db->prepare("SELECT r_id, val FROM table WHERE levenshtein_ratio(:input, someval) > 70");
$stmt->execute(array('input' => $input));
$result = $stmt->fetchAll();
if(count($result)) {
foreach($result as $row) {
$out .= $row['r_id'] . ', ' . $row['val'];
}
}
And it works a treat, exactly as expected. But I was wondering, is there a nice way to also get the value that levenshtein_ratio() calculates?
I've tried:
$stmt = $db->prepare("SELECT levenshtein_ratio(:input, someval), r_id, val FROM table WHERE levenshtein_ratio(:input, someval) > 70");
$stmt->execute(array('input' => $input));
$result = $stmt->fetchAll();
if(count($result)) {
foreach($result as $row) {
$out .= $row['r_id'] . ', ' . $row['val'] . ', ' . $row[0];
}
}
and it does technically work (I get the percentage from the $row[0]), but the query is a bit ugly, and I can't use a proper key to get the value, like I can for the other two items.
Is there a way to somehow get a nice reference for it?
I tried:
$stmt = $db->prepare("SELECT r_id, val SET output=levenshtein_ratio(:input, someval) FROM table WHERE levenshtein_ratio(:input, someval) > 70");
modelling it after something I found online, but it didn't work, and ends up ruining the whole query.
Speeding It Up:
I'm running this query for an array of values:
foreach($parent as $input){
$stmt = ...
$stmt->execute...
$result = $stmt->fetchAll();
... etc
}
But it ends up being remarkably slow. Like 20s slow, for an array of only 14 inputs and a DB with about 350 rows, which is expected to be in the 10,000's soon. I know that putting queries inside loops is naughty business, but I'm not sure how else to get around it.
EDIT 1
When I use
$stmt = $db->prepare("SELECT r_id, val SET output=levenshtein_ratio(:input, someval) FROM table WHERE levenshtein_ratio(:input, someval) > 70");
surely that's costing twice the time as if I only calculated it once? Similar to having $i < sizeof($arr); in a for loop?
To clean up the column names you can use "as" to rename the column of the function. At the same time you can speed things up by using that column name in your where clause so the function is only executed once.
$stmt = $db->prepare("SELECT r_id, levenshtein_ratio(:input, someval) AS val FROM table HAVING val > 70");
If it is still too slow you might consider a c library like https://github.com/juanmirocks/Levenshtein-MySQL-UDF
doh - forgot to switch "where" to "having", as spencer7593 noted.
I'm assuming that `someval` is an unqalified reference to a column in the table. While you may understand that without looking at the table definition, someone else reading the SQL statement can't tell. As an aid to future readers, consider qualifying your column references with the name of the table or (preferably) a short alias assigned to the table in the statement.
SELECT t.r_id
, t.val
FROM `table` t
WHERE levenshtein_ratio(:input, t.someval) > 70
That function in the WHERE clause has to be evaluated for every row in the table. There's no way to get MySQL to build an index on that. So there's no way to get MySQL to perform an index range scan operation.
It might be possible to get MySQL to use an index for the query, for example, if the query had an ORDER BY t.val clause, or if there is a "covering index" available.
But that doesn't get around the issue of needing to evaluate the function for every row. (If the query had other predicates that excluded rows, then the function wouldn't necessarily need be evaluated for the excluded rows.)
Adding the expression to the SELECT list really shouldn't be too expensive if the function is declared to be DETERMINISTIC. A second call to a DETERMINISTIC function with the same arguments can reuse the value returned for the previous execution. (Declaring a function DETERMINISTIC essentially means that the function is guaranteed to return the same result when given the same argument values. Repeated calls will return the same value. That is, the return value depends only the argument values, and doesn't depend on anything else.
SELECT t.r_id
, t.val
, levenshtein_ratio(:input, t.someval) AS lev_ratio
FROM `table` t
WHERE levenshtein_ratio(:input2, t.someval) > 70
(Note: I used a distinct bind placeholder name for the second reference because PDO doesn't handle "duplicate" bind placeholder names as we'd expect. (It's possible that this has been corrected in more recent versions of PDO. The first "fix" for the issue was an update to the documentation noting that bind placeholder names should appear only once in statement, if you needed two references to the same value, use two different placeholder names and bind the same value to both.)
If you don't want to repeat the expression, you could move the condition from the WHERE clause to the HAVING, and refer to the expression in the SELECT list by the alias assigned to the column.
SELECT t.r_id
, t.val
, levenshtein_ratio(:input, t.someval) AS lev_ratio
FROM `table` t
HAVING lev_ratio > 70
The big difference between WHERE and HAVING is that the predicates in the WHERE clause are evaluated when the rows are accessed. The HAVING clause is evaluated much later, after the rows have been accessed. (That's a brief explanation of why the HAVING clause can reference columns in the SELECT list by their alias, but the WHERE clause can't do that.)
If that's a large table, and a large number of rows are being excluded, there might be a significant performance difference using the HAVING clause.. there may be a much larger intermediate set created.
To get an "index used" for the query, a covering index is the only option I see.
ON `table` (r_id, val, someval)
With that, MySQL can satisfy the query from the index, without needing to lookup pages in the underlying table. All of the column values the query needs are available from the index.
FOLLOWUP
To get an index created, we would need to create a column, e.g.
lev_ratio_foo FLOAT
and pre-populate with the result from the function
UPDATE `table` t
SET t.lev_ratio_foo = levenshtein_ratio('foo', t.someval)
;
Then we could create an index, e.g.
... ON `table` (lev_ratio_foo, val, r_id)
And re-write the query
SELECT t.r_id
, t.val
, t.lev_ratio_foo
FROM `table` t
WHERE t.lev_ratio_foo > 70
With that query, MySQL can make use of an index range scan operation on an index with lev_ratio_foo as the leading column.
Likely, we would want to add BEFORE INSERT and BEFORE UPDATE triggers to maintain the value, when a new row is added to the table, or the value of the someval column is modified.
That pattern could be extended, additional columns could be added for values other than 'foo'. e.g. 'bar'
UPDATE `table` t
SET t.lev_ratio_bar = levenshtein_ratio('bar', t.someval)
Obviously that approach isn't going to be scalable for a broad range of input values.
I am in a bit of a pickle. I have a stored procedure that accepts an argument and returns the results of a query. The query is using an IN statement.
Here's the structure of the SP:
CREATE OR REPLACE
PROCEDURE EXAMPLE_SP
(
arg VARCHAR2,
argRS1 OUT cursors.rs
)
AS
l_test VARCHAR2(255) := arg;
BEGIN
OPEN argRS1 FOR
SELECT * FROM TABLE1
WHERE LOCATION IN (l_test);
END EXAMPLE_SP;
The number of values within the IN statement can be variable. The options for IN are coming from selected form checkboxes on the UI side.
I am using PHP to retrieve the selected checkbox values. I have already tried imploding the values into a comma deliminated string.
My logic for that was that the query would then look like this:
l_test = 'val1, val2, val3';
SELECT * FROM TABLE1
WHERE LOCATION IN (val1, val2, val3);
But that didn't work. I am not sure how to proceed. Thanks in advance for any constructive comments or suggestions.
You can add this comma separated input parameter as a varchar() and use following where statement:
where (','||l_test||',' like '%,'||LOCATION||',%')
for example if l_test='2,3,4,5' and LOCATION=3 we get:
where (',2,3,4,5,' like '%,3,%')
and it's TRUE if LOCATION value is in this list.
I think the location that you have selected is VARCHAR,so for that you need to convert the list as shown below
l_test = '''val1''||','|| ''val2''||','||''val3''';
So that your final query look like
SELECT * FROM TABLE1
WHERE LOCATION IN ('val1', 'val2', 'val3');
You can do like this also
CREATE OR REPLACE
PROCEDURE EXAMPLE_SP
(
arg VARCHAR2,
argRS1 OUT cursors.rs
)
AS
l_test VARCHAR2(255) := arg;
BEGIN
l_test:=''''||replace(l_test,',',''',''')||'''';
OPEN argRS1 FOR
SELECT * FROM TABLE1
WHERE LOCATION IN (l_test);
END EXAMPLE_SP;
Note:I have not tested this ,but i think this way you will achieve what you want
I would do this without using string manipulation. Theoretically there may currently be little risk of SQL Injection because you're using checkboxes it's best to implement good practice at the beginning so if anything changes you don't put yourself at risk.
The second benefit is that you are still able to utilise any indexes on your column, which you wouldn't be able to do if you use like '%....
To do this you can utilise a table function and an external object to populate your "in" list.
As an example I'm going to return the OBJECT_NAME from USER_OBJECTS.
If I create two tables:
create table tmp_test ( a number );
create table tmp_test2 ( a number );
and an object to hold the list of tables, or in your case locations.
create type t_test_object is table of varchar2(30);
Next, here's the equivalent of your procedure. It's a function that returns a SYS_REFCURSOR. It accepts the T_TEST_OBJECT as a parameter, which means this first needs to be populated before being passed to the function.
create or replace function select_many (
Ptest_object in t_test_object
) return sys_refcursor is
l_curs sys_refcursor;
begin
open l_curs for
select object_name
from user_objects
where object_name in ( select *
from table(Ptest_object)
);
return l_curs;
end;
Lastly, here's an example of how to use this set-up. Notice how an instance of T_TEST_OBJECT gets populated with multiple values. This object then gets passed to the function to return your cursor. Lastly, to display the values I loop through the cursor. Obviously you may want to utilise the cursor and populate the TYPE differently.
SQL> declare
2
3 l_nested_table t_test_object := new t_test_object();
4 l_cursor sys_refcursor;
5 -- Used for display demonstration only.
6 l_object_name user_objects.object_name%type;
7
8 begin
9
10 l_nested_table.extend;
11 l_nested_table(l_nested_table.last) := 'TMP_TEST';
12 l_nested_table.extend;
13 l_nested_table(l_nested_table.last) := 'TMP_TEST2';
14
15 l_cursor := select_many(l_nested_table);
16
17 loop -- Display example, not really relevant
18 fetch l_cursor into l_object_name;
19 exit when l_cursor%notfound;
20 dbms_output.put_line(l_object_name);
21 end loop;
22
23 end;
24 /
TMP_TEST
TMP_TEST2
PL/SQL procedure successfully completed.
You can use Oracle examples from Oracle Documentation: http://docs.oracle.com/cd/B28359_01/win.111/b28378/basfeat.htm#sthref207
Look here - return a table:
Can an SQL procedure return a table?
And here's another example:
PACKAGE emp_actions IS
TYPE EnameTabTyp IS TABLE OF emp.ename%TYPE INDEX BY BINARY_INTEGER;
TYPE SalTabTyp IS TABLE OF emp.sal%TYPE INDEX BY BINARY_INTEGER;
...
PROCEDURE hire_batch (ename_tab IN EnameTabTyp, sal_tab IN SalTabTyp, ...);
PROCEDURE log_names (ename_tab IN EnameTabTyp);
END emp_actions;
I'm programming a script using PHP and MySQL and I want to get a
unique id (consisting of a string: capitals and small
letters with numbers) like: gHYtUUi5b.
I found many functions in PHP that can generate such numbers but I'm afraid about how to ensure the id is unique!
UPDATE: uuid is long, I mean such id like: (P5Dc) an 11 alphanumeric char.
EDIT: This answer has been flagged for being dangerous in the context of destroying a database. Do NOT use this code to generate unique ids in databases!
I use UUID() to create a unique value.
example:
insert into Companies (CompanyID, CompanyName) Values(UUID(), "TestUUID");
You may like the way that we do it. I wanted a reversible unique code that looked "random" -a fairly common problem.
We take an input number such as 1,942.
Left pad it into a string: "0000001942"
Put the last two digits onto the front: "4200000019"
Convert that into a number: 4,200,000,019
We now have a number that varies wildly between calls and is guaranteed to be less than 10,000,000,000. Not a bad start.
Convert that number to a Base 34 string: "2oevc0b"
Replace any zeros with 'y' and any ones with 'z': "2oevcyb"
Upshift: "2OEVCYB"
The reason for choosing base 34 is so that we don't worry about 0/O and 1/l collisions. Now you have a short random-looking key that you can use to look up a LONG database identifier.
A programmatic way can be to:
add a UNIQUE INDEX to the field
generate a random string in PHP
loop in PHP ( while( ! DO_THE_INSERT ) )
generate another string
Note:
This can be dirty, but has the advantage to be DBMS-agnostic
Even if you choose to use a DBMS specific unique ID generator function (UUID, etc)
it is a best practice to assure the field HAS to be UNIQUE, using the index
the loop is statistically not executed at all, it is entered only on insert failure
If you use MySQL with version higher than 5.7.4, you can use the newly added RANDOM_BYTES function:
SELECT TO_BASE64(RANDOM_BYTES(16));
This will result in a random string such as GgwEvafNLWQ3+ockEST00A==.
How you generate the unique_ids is a useful question - but you seem to be making a counter productive assumption about when you generate them!
My point is that you do not need to generate these unique id's at the time of creating your rows, because they are essentially independent of the data being inserted.
What I do is pre-generate unique id's for future use, that way I can take my own sweet time and absolutely guarantee they are unique, and there's no processing to be done at the time of the insert.
For example I have an orders table with order_id in it. This id is generated on the fly when the user enters the order, incrementally 1,2,3 etc forever. The user does not need to see this internal id.
Then I have another table - unique_ids with (order_id, unique_id). I have a routine that runs every night which pre-loads this table with enough unique_id rows to more than cover the orders that might be inserted in the next 24 hours. (If I ever get 10000 orders in one day I'll have a problem - but that would be a good problem to have!)
This approach guarantees uniqueness and takes any processing load away from the insert transaction and into the batch routine, where it does not affect the user.
Use UUID function.
I don't know the source of your procedures in PHP that generates unique values. If it is library function they should guarantee that your value is really unique. Check in documentation. You should, hovewer, use this function all the time. If you, for example, use PHP function to generate unique value, and then you decide to use MySQL function, you can generate value that already exist. In this case putting UNIQUE INDEX on the column is also a good idea.
DELIMITER $$
USE `temp` $$
DROP PROCEDURE IF EXISTS `GenerateUniqueValue`$$
CREATE PROCEDURE `GenerateUniqueValue`(IN tableName VARCHAR(255),IN columnName VARCHAR(255))
BEGIN
DECLARE uniqueValue VARCHAR(8) DEFAULT "";
DECLARE newUniqueValue VARCHAR(8) DEFAULT "";
WHILE LENGTH(uniqueValue) = 0 DO
SELECT CONCAT(SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1),
SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', RAND()*34+1, 1)
) INTO #newUniqueValue;
SET #rcount = -1;
SET #query=CONCAT('SELECT COUNT(*) INTO #rcount FROM ',tableName,' WHERE ',columnName,' like ''',newUniqueValue,'''');
PREPARE stmt FROM #query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
IF #rcount = 0 THEN
SET uniqueValue = #newUniqueValue ;
END IF ;
END WHILE ;
SELECT uniqueValue;
END$$
DELIMITER ;
And call the stored procedure as GenerateUniqueValue('tableName','columnName'). This will give you a 8 digit unique character everytime.
To get unique and random looking tokens you could just encrypt your primary key i.e.:
SELECT HEX(AES_ENCRYPT(your_pk,'your_password')) AS 'token' FROM your_table;
This is good enough plus its reversable so you'd not have to store that token in your table but to generate it instead.
Another advantage is once you decode your PK from that token you do not have to do heavy full text searches over your table but simple and quick PK search.
Theres one small problem though. MySql supports different block encryption modes which if changed will completely change your token space making old tokens useless...
To overcome this one could set that variable before token generated i.e.:
SET block_encryption_mode = 'aes-256-cbc';
However that a bit waste... The solution for this is to attach an encryption mode used marker to the token:
SELECT CONCAT(CONV(CRC32(##GLOBAL.block_encryption_mode),10,35),'Z',HEX(AES_ENCRYPT(your_pk,'your_password'))) AS 'token' FROM your_table;
Another problem may come up if you wish to persist that token in your table on INSERT because to generate it you need to know primary_key for the record which was not inserted yet... Ofcourse you might just INSERT and then UPDATE with LAST_INSERT_ID() but again - theres a better solution:
INSERT INTO your_table ( token )
SELECT CONCAT(CONV(CRC32(##GLOBAL.block_encryption_mode),10,35),'Z',HEX(AES_ENCRYPT(your_pk,'your_password'))) AS 'token'
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = "your_table";
One last but not least advantage of this solution is you can easily replicate it in php, python, js or any other language you might use.
Below is just for reference of numeric unique random id...
it may help you...
$query=mysql_query("select * from collectors_repair");
$row=mysql_num_rows($query);
$ind=0;
if($row>0)
{
while($rowids=mysql_fetch_array($query))
{
$already_exists[$ind]=$rowids['collector_repair_reportid'];
}
}
else
{
$already_exists[0]="nothing";
}
$break='false';
while($break=='false'){
$rand=mt_rand(10000,999999);
if(array_search($rand,$alredy_exists)===false){
$break='stop';
}else{
}
}
echo "random number is : ".$echo;
and you can add char with the code like -> $rand=mt_rand(10000,999999) .$randomchar; // assume $radomchar contains char;
For uniqueness what I do is I take the Unix timestamp and append a random string to it and use that.
<?php
$hostname_conn = "localhost";
$database_conn = "user_id";
$username_conn = "root";
$password_conn = "";
$conn = mysql_pconnect($hostname_conn, $username_conn, $password_conn) or trigger_error(mysql_error(),E_USER_ERROR);
mysql_select_db($database_conn,$conn);
// run an endless loop
while(1) {
$randomNumber = rand(1, 999999);// generate unique random number
$query = "SELECT * FROM tbl_rand WHERE the_number='".mysql_real_escape_string ($randomNumber)."'"; // check if it exists in database
$res =mysql_query($query,$conn);
$rowCount = mysql_num_rows($res);
// if not found in the db (it is unique), then insert the unique number into data_base and break out of the loop
if($rowCount < 1) {
$con = mysql_connect ("localhost","root");
mysql_select_db("user_id", $con);
$sql = "insert into tbl_rand(the_number) values('".$randomNumber."')";
mysql_query ($sql,$con);
mysql_close ($con);
break;
}
}
echo "inserted unique number into Data_base. use it as ID";
?>
crypt() as suggested and store salt in some configuration file, Start salt from 1 and if you find duplicate move to next value 2. You can use 2 chars, but that will give you enough combination for salt.
You can generate string from openssl_random_pseudo_bytes(8). So this should give random and short string (11 char) when run with crypt().
Remove salt from result and there will be only 11 chars that should be enough random for 100+ millions if you change salt on every fail of random.
You might also consider using crypt()* to generate a [nearly-guaranteed] unique ID inside your contraints.
USE IT
$info = random_bytes(16);
$info[6] = chr(ord($info[6]) & 0x0f | 0x40);
$info[8] = chr(ord($info[8]) & 0x3f | 0x80);
$result =vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($info), 4));
return $result;
This generates random ids:
CREATE TABLE Persons (
ID Integer PRIMARY KEY AUTOINCREMENT,
LastName varchar(255) NOT NULL,
FirstName varchar(255),
Age int
);
You could use Twitter's snowflake.
In short, it generates a unique id based on time, server id and a sequence. It generates a 64-bit value so it is pretty small and it fits in an INT64. It also allows for sorting values correctly.
https://developer.twitter.com/en/docs/basics/twitter-ids
In sum, it allows multiple servers, highly concurrency, sorting value and all of them in 64 bits.
Here it is the implementation for MySQL
https://github.com/EFTEC/snowflake-mysql
It consists of a function and a table.