包含特殊数字格式的字符串列的AUTO_INCREMENT实现

时间:2013-12-16 03:54:13

标签: php mysql sql

上下文和目标

在表格clients中,我有一个clientNum CHAR(11) NOT NULLUNIQUE KEY约束。它包含xxx-xxx-xxx格式的客户编号,其中x是十进制数字。有关格式的更多详细信息,请参阅下文。

我想为此列实现AUTO_INCREMENT之类的内容,以便每个客户端自动计算其编号。来自MySQL CREATE TABLE docs

  

整数或浮点列可以具有附加属性AUTO_INCREMENT。将值NULL(推荐)或0插入索引AUTO_INCREMENT列时,该列将设置为下一个序列值。通常,这是value+1,其中value是当前表中列的最大值。 AUTO_INCREMENT序列以1开头。

所以我想找到下一个可用的数字,并将其用作新插入的客户行的clientNum值。可用的下一个数字是当前最大值clientNum递增。

我使用PDO编写PHP代码来访问MySQL数据库(参见PDO Tutorial for MySQL Developers)。

客户端编号格式

如上所述,客户编号的格式为xxx-xxx-xxx,其中x是十进制数字。每个细分的范围为000999。它基本上是一个9位整数,前导零和短划线为千位分隔符。它不能超过999-999-999

目前我们希望它受到更多限制,特别是格式000-1xx-xxx000-100-000000-199-999之间)。但是数据库中已经有一些数字可以从000-000-001500-000-000开始。

不幸的是,它必须以这种格式存储,我无法改变它。

查找最大值

我需要将000-100-000范围内的最大数量设为000-199-999,必须忽略此范围之外的值。这就是我的问题所在,因为如前所述,在此之前已有一些数字存在。

最大值永远不会是000-199-999。否则会导致添加000-200-000,并且下次再次调用最大值将000-199-999,导致尝试再次插入000-200-000

递增的工作原理

在PHP中可以这样做:

$clientNum = "000-100-000";
$clientNum = str_replace("-", "", $clientNum);
$clientNum++;
$clientNum = implode("-", str_split(str_pad($clientNum, 9, "0", STR_PAD_LEFT), 3));

最终$clientNum值为000-100-001

当初始数字为000-120-015时,上面的代码会生成000-120-016。溢出传播到下一个片段,即000-100-999变为000-101-000999-999-999无法递增。

开头的想法

在循环中我想获得下一个数字,检查数据库中是否存在该数字,如果是,则重做该循环直到找到未使用的数字。我知道如何第一次检查它是否在数据库中,但我不知道如何进行循环。

有没有人知道这样做的方法?

3 个答案:

答案 0 :(得分:1)

您可能希望在SQL中解决此问题,因为否则您需要两个事务(一个用于读取,一个用于写入),同时该数字可以由并发访问使用。

在MySQL中,您可以使用PHP代码的SQL重新实现:

INSERT(INSERT(LPAD(CAST(CAST(REPLACE(clientNum, '-', '') as UNSIGNED) + 1 as CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-')

这会将000-000-999增加到000-001-000,将999-999-999增加到100-000-000(从100-000-0000 LPAD()截断。我警告过你。

E.g。要预览下一个值是什么,请使用

SELECT INSERT(INSERT(LPAD(CAST(CAST(REPLACE(clientNum, '-', '') as UNSIGNED) + 1 as CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-') FROM clients

如果要在插入新行时使用此项,则使用如下:

INSERT
INTO clients(clientNum, name)
SELECT
    INSERT(INSERT(LPAD(CAST(
        COALESCE(MAX(CAST(REPLACE(clientNum, '-', '') AS UNSIGNED)), 0) + 1
    AS CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-'),
    'John Doe'
FROM clients

无论您使用什么API来访问数据库,这都有效,只要它是MySQL数据库。数据库进行计算。但是,如果clients是一个临时表,它不起作用,我希望它不是。更多关于以下内容。

另请参阅MySQL手册中的string functionsCAST()COALESCE()INSERT … SELECT


稍后您添加了允许的值范围为000-100-000000-199-999。为了找到最大值,应忽略其他值。必须在WHERE上面写的SELECT部分添加INSERT子句。

INSERT
INTO clients(clientNum, name)
SELECT
    INSERT(INSERT(LPAD(CAST(
        COALESCE(MAX(CAST(REPLACE(clientNum, '-', '') AS UNSIGNED)), 0) + 1
    AS CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-'),
    'John Doe'
FROM clients
WHERE clientNum BETWEEN '000-100-000' AND '000-199-999'

然后你说我的解决方案不适合你和proposed a supposed fix

INSERT
INTO clients(clientNum, name)
VALUES
    (SELECT
        INSERT(INSERT(LPAD(CAST(
            COALESCE(MAX(CAST(REPLACE(clientNum, '-', '') AS UNSIGNED)), 0) + 1
        AS CHAR), 9, '0'), 7, 0, '-'), 4, 0, '-')
        FROM clients AS tmptable
        WHERE clientNum BETWEEN '000-100-000' AND '000-199-999'),
    'John Doe'

这使用subquery代替INSERT … SELECT语法。

在MySQL中,无法修改表(在本例中为INSERT)并且同时由子查询读取。引用子查询手册:

  

在MySQL中,您无法修改表并从子查询中的同一个表中进行选择。这适用于DELETEINSERTREPLACEUPDATE等语句,以及(因为子查询可以在SET子句中使用){{3} }。

但是,您找到了LOAD DATA INFILE。当定义别名(在这种情况下为clients AS tmptable)时,将使用临时表,该别名同时避免读取和写入同一个表。您使用临时表来存储原始表,描述变通方法的文章使用它来存储子查询的结果(我猜这更有效)。这两种方法都有效。

此时我想指出我的解决方案应该有效(和workaround using a temporary table!),除了clients是临时表的不可能的情况。我想我可以期待它不是一个。引用INSERT … SELECT手册页:

  

当同时从表中选择并插入表时,MySQL会创建一个临时表来保存works for me中的行,然后将这些行插入到目标表中。但是,当INSERT INTO t ... SELECT ... FROM tt表时,您仍然无法使用TEMPORARY,因为TEMPORARY表在同一语句中不能被引用两次(请参阅{{3 }})。

至于我,这明确地说我使用INSERT … SELECT的原始方法应该有效。


为了提供完整的答案,我将使用数据库轮询解决您对PHP解决方案的原始请求。我必须补充一点,这肯定不是一个好的解决方案。

您的clientNum列必须是唯一键。您需要重复以下步骤,直到成功更新:

  1. 获取当前最大值clientNum
  2. 增加获得的值。
  3. 尝试插入行。
  4. 如果成功,请完成,否则将clientNum最大值抛出并循环。
  5. 由于违反上述唯一键约束,插入将失败。当数据库的另一个连接在步骤 1。 3 之间同时成功执行插入时,会发生这种情况。

    您应该使用SELECT在循环外准备语句,然后在循环中使用Section C.5.7.2, “TEMPORARY Table Problems”execute方法的返回值表示成功(true)或失败(false)。

    这足以实现步骤 3。。步骤 1。 2。包括获取

    的结果
    SELECT MAX(clientNum) FROM clients
    

    并通过PDO::prepare()提供的代码运行它。步骤 4。是一个简单的循环条件,使用步骤 3中执行INSERT查询的返回值。

答案 1 :(得分:1)

<?php
    mysql_connect(....);
    mysql_select_db($db_name);
    $res=mysql_query("select ClientNum from ClientTable");
    $name_arr=array();
    while($row=mysql_fetch_array($res))
    {
        foreach($row as $name)
            $name_arr[]=$name;
    }

    $clientNum="000-000-000";
    while(true){
        $clientNum = str_replace("-", "", $clientNum);
        $clientNum++;
        if($clientNum>999999999)
        {
                echo("No mismatch found");
                break;
        }
        $clientNum = implode("-", str_split($clientNum, 3));
        if(!in_array($clientNum, $name_arr))
        {
                echo "The first unmatched clientNum is:".$clientNum;//This is what you want.
                break;
        }
    }
?>

评论

  1. 查询只执行一次,即在while循环之外,使得时间复杂化程度降低。由于使用数组而不是多次执行查询本身,时间复杂度降低了,因为在数组中搜索比在数据库中搜索的时间复杂得多。

答案 2 :(得分:0)

基于您提供的功能的简单解决方案。 将函数名称 rawr()更改为您喜欢的任何命名。 (我找不到最好的名字,最后使用了一些乱码名称lol)。

function rawr($in)
{
    $num = str_replace("-", "", $in);
    $num++;

    // Convert back
    $str = (string) $num;

    // Add Leading 0
    while(strlen($str) < 9)
    {
        $str = "0" . $str;
    }
    echo $str . "<br />";
    $final = substr($str,0,3) . "-" . substr($str,3,3) . "-" . substr($str,6,3);

    return $final;
}

要测试它,请尝试以下代码:

echo rawr(0);
echo "<br />";
echo rawr("000-000-000");
echo "<br />";
echo rawr("012-345-678");
echo "<br />";
echo rawr("123-456-789");
echo "<br />";

这将提供您想要的输出。但是,您必须自己编写代码才能测试数据库。在我看来,这不是解决问题的最佳方法,但它应该有效:)