Php MySQL-字符串索引

Php MySQL-字符串索引,php,mysql,string,performance,indexing,Php,Mysql,String,Performance,Indexing,我正在寻找一种在字符串上设置唯一约束的最佳方法 我的用例是通过SMTP提要上的“邮件ID”和“回复”字段将所有电子邮件链接在一起 然而,由于邮件的数量可能会增长到数百万条,而且我们没有删除任何内容的计划,因此我需要一种快速索引它们的方法。问题是我的印象是字符串在索引这些数字时天生就比较慢(如果我错了,请解释) 到目前为止,我的解决方案是将消息ID转换为sha256散列,然后转换为8 x 32位块256位数字,如下所示: // Its not actually written in C struc

我正在寻找一种在字符串上设置唯一约束的最佳方法

我的用例是通过SMTP提要上的“邮件ID”和“回复”字段将所有电子邮件链接在一起

然而,由于邮件的数量可能会增长到数百万条,而且我们没有删除任何内容的计划,因此我需要一种快速索引它们的方法。问题是我的印象是字符串在索引这些数字时天生就比较慢(如果我错了,请解释)

到目前为止,我的解决方案是将消息ID转换为
sha256
散列,然后转换为8 x 32位块256位数字,如下所示:

// Its not actually written in C
struct message_id {
    int32_t id;
    char[255] originalMessageId;

    int32_t p01;
    int32_t p02;
    int32_t p03;
    int32_t p04;
    int32_t p05;
    int32_t p06;
    int32_t p07;
    int32_t p08;
}
然后在所有节点上放置唯一约束

现在,在任何人谈论消息ID的唯一性质量之前,我知道,但是这个系统并没有设计成具有高完整性,只是具有高性能

所以我的问题是:

这就足够了,还是我错过了在MySql中索引字符串的技巧

编辑:添加模型设计

MessageIdentity.php

/**
 * MessageIdentity
 *
 * @ORM\Table(name="inbox_message_identity",
 *      uniqueConstraints={
 *          @ORM\UniqueConstraint(columns={
 *              "p01", "p02", "p03", "p04",
 *              "p05", "p06", "p07", "p08"
 *          })
 *      })
 * @ORM\Entity(repositoryClass="AppBundle\Entity\Inbox\MessageIdentityRepository")
 */
class MessageIdentity
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="originalMessageId", type="string", length=255)
     */
    private $originalMessageId;

    /**
     * @var integer
     *
     * @ORM\Column(name="p01", type="integer")
     */
    private $p01;

    /**
     * @var integer
     *
     * @ORM\Column(name="p02", type="integer")
     */
    private $p02;

    /**
     * @var integer
     *
     * @ORM\Column(name="p03", type="integer")
     */
    private $p03;

    /**
     * @var integer
     *
     * @ORM\Column(name="p04", type="integer")
     */
    private $p04;

    /**
     * @var integer
     *
     * @ORM\Column(name="p05", type="integer")
     */
    private $p05;

    /**
     * @var integer
     *
     * @ORM\Column(name="p06", type="integer")
     */
    private $p06;

    /**
     * @var integer
     *
     * @ORM\Column(name="p07", type="integer")
     */
    private $p07;

    /**
     * @var integer
     *
     * @ORM\Column(name="p08", type="integer")
     */
    private $p08;

    /**
     * @param $string
     */
    public function __construct($string)
    {
        parent::__construct();

        $bits = self::createBits($this->originalMessageId = $string);

        $this->p01 = $bits[0];
        $this->p02 = $bits[1];
        $this->p03 = $bits[2];
        $this->p04 = $bits[3];
        $this->p05 = $bits[4];
        $this->p06 = $bits[5];
        $this->p07 = $bits[6];
        $this->p08 = $bits[7];
    }

    public static function createBits($string)
    {
        $hash = hash('sha256', $string);
        $bits = array();


        // Bits are packed in pairs of 16 bit chunks before unpacking as signed 32 bit chunks
        // in order to guarrentee there is no overflow when converting the unsigned hex number into a
        // PHP integer on 32 bit machines.
        $bits[] = self::pluck(unpack('l', pack('s', hexdec(substr($hash, 0,  4))) . pack('s', hexdec(substr($hash, 4,  4)))));
        $bits[] = self::pluck(unpack('l', pack('s', hexdec(substr($hash, 8,  4))) . pack('s', hexdec(substr($hash, 12, 4)))));
        $bits[] = self::pluck(unpack('l', pack('s', hexdec(substr($hash, 16, 4))) . pack('s', hexdec(substr($hash, 20, 4)))));
        $bits[] = self::pluck(unpack('l', pack('s', hexdec(substr($hash, 24, 4))) . pack('s', hexdec(substr($hash, 28, 4)))));
        $bits[] = self::pluck(unpack('l', pack('s', hexdec(substr($hash, 32, 4))) . pack('s', hexdec(substr($hash, 36, 4)))));
        $bits[] = self::pluck(unpack('l', pack('s', hexdec(substr($hash, 40, 4))) . pack('s', hexdec(substr($hash, 44, 4)))));
        $bits[] = self::pluck(unpack('l', pack('s', hexdec(substr($hash, 48, 4))) . pack('s', hexdec(substr($hash, 52, 4)))));
        $bits[] = self::pluck(unpack('l', pack('s', hexdec(substr($hash, 56, 4))) . pack('s', hexdec(substr($hash, 60, 4)))));

        return $bits;
    }

    protected static function pluck($array)
    {
        return $array[1];
    }
}
MessageIdentityRepository.php

class MessageIdentityRepository extends \Doctrine\ORM\EntityRepository
{
    public function getExisting($string)
    {
        $bits = MessageIdentity::createBits($string);

        $qb = $this->createQueryBuilder('i');
        $qb
            ->where($qb->expr()->andX(
                $qb->expr()->eq('i.p01', $qb->expr()->literal($bits[0])),
                $qb->expr()->eq('i.p02', $qb->expr()->literal($bits[1])),
                $qb->expr()->eq('i.p03', $qb->expr()->literal($bits[2])),
                $qb->expr()->eq('i.p04', $qb->expr()->literal($bits[3])),
                $qb->expr()->eq('i.p05', $qb->expr()->literal($bits[4])),
                $qb->expr()->eq('i.p06', $qb->expr()->literal($bits[5])),
                $qb->expr()->eq('i.p07', $qb->expr()->literal($bits[6])),
                $qb->expr()->eq('i.p08', $qb->expr()->literal($bits[7]))
            ))
            ->setMaxResults(1)
        ;

        return $qb->getQuery()->getOneOrNullResult();
    }
}
class MessageRepository extends \Doctrine\ORM\EntityRepository
{
    public function getLastWithMessageID(MessageIdentity $messageIdentity)
    {
        $qb = $this->createQueryBuilder('m');
        $qb
            ->where('m.messageIdentity = :identity')
            ->setParameter(':identity', $messageIdentity)
            ->orderBy('m.date', 'DESC')
            ->setMaxResults(1)
        ;
        return $qb->getQuery()->getOneOrNullResult();
    }
}
MessageRepository.php

class MessageIdentityRepository extends \Doctrine\ORM\EntityRepository
{
    public function getExisting($string)
    {
        $bits = MessageIdentity::createBits($string);

        $qb = $this->createQueryBuilder('i');
        $qb
            ->where($qb->expr()->andX(
                $qb->expr()->eq('i.p01', $qb->expr()->literal($bits[0])),
                $qb->expr()->eq('i.p02', $qb->expr()->literal($bits[1])),
                $qb->expr()->eq('i.p03', $qb->expr()->literal($bits[2])),
                $qb->expr()->eq('i.p04', $qb->expr()->literal($bits[3])),
                $qb->expr()->eq('i.p05', $qb->expr()->literal($bits[4])),
                $qb->expr()->eq('i.p06', $qb->expr()->literal($bits[5])),
                $qb->expr()->eq('i.p07', $qb->expr()->literal($bits[6])),
                $qb->expr()->eq('i.p08', $qb->expr()->literal($bits[7]))
            ))
            ->setMaxResults(1)
        ;

        return $qb->getQuery()->getOneOrNullResult();
    }
}
class MessageRepository extends \Doctrine\ORM\EntityRepository
{
    public function getLastWithMessageID(MessageIdentity $messageIdentity)
    {
        $qb = $this->createQueryBuilder('m');
        $qb
            ->where('m.messageIdentity = :identity')
            ->setParameter(':identity', $messageIdentity)
            ->orderBy('m.date', 'DESC')
            ->setMaxResults(1)
        ;
        return $qb->getQuery()->getOneOrNullResult();
    }
}
这是一个使用Doctrine2构建的模型。消息本身持有
MessageIdentity
表的外键

MessageIdentity
是通过重构位集并搜索所有列来搜索的,这些列应该完全利用表上的唯一约束


根据映射的标识(按降序日期排序)搜索邮件,只提取一行。

您正在解决一个不存在的问题

当然,比较两个字符串要比比较两个
int
慢一点。但是速度不够慢,不能保证用MD5/SHA1/等站在你的头上。这样做的所有开销都会比字符串的速度慢得多

另一方面,如果您计划使用长度超过767字节的唯一字符串,则需要做一些事情。如果是这样,我将讨论一些解决办法

同时,我认为SHA256是严重的过度使用。对于128位MD5,“在一个包含9万亿字符串的表中,9万亿次错误复制中有一次机会。”对于仅“数百万次”的情况,可能性更为渺茫

还有一点<代码>二进制(20)和
CHAR(20)COLLATE…\u bin
的处理方式相同。更复杂的排序需要更多的努力

附录

背景:实际上MySQL中唯一的索引类型是BTree。这是一个有序的列表。它对于“点查询”(给定一个键,查找记录)也非常有效。对于InnoDB,BTree块是16KB的块。对于一百万行来说,BTree大约有3层深。一万亿——六

SHA256/UUID/GUID/其他摘要——当您有非常多的行时,所有这些都会执行得非常糟糕。这是因为它们是非常随机的,在非常大的表的情况下,您不可能将所需的块缓存在RAM中

以下是一个折衷的解决方案,以人工表为例:

CREATE TABLE foo (
    in_reply_to VARCHAR(500) NOT NULL CHARACTER SET ...,
    ...
    KEY(in_reply_to(20))
    PRIMARY KEY (...)
) ENGINE=InnoDB;

SELECT ... FROM foo
    WHERE in_reply_to = '...';
请注意,我没有sha256或其他摘要

KEY(在回复(20)中)
用该字段的前20个字符建立索引。
UNIQUE(在对(20)的回复中)
也会这样做,但也会将20个字符限制为唯一。这不是你想要的

以下是我的
SELECT
的工作原理:

  • 查看该键,找到与前20个字符匹配的1行、2行或几行
  • 验证回复中的整个
    是否与搜索字符串完全匹配
  • 交付结果集
  • 最好选择“20”作为双方之间的一个良好折衷方案

    • 足够长,通常是独一无二的
    • 短到足以使钥匙的B树保持小。(更小-->更可缓存-->更快)
    您可能已经注意到缺少一件事——MySQL没有进行唯一性检查;您的代码必须通过执行my
    SELECT
    的某些变体来实现这一点


    SHA256有256位,因此需要
    二进制(32)
    。即使使用1e25不同的文档,仍有大约1/1e25的文档存在虚假副本。

    您正在解决一个不存在的问题

    当然,比较两个字符串要比比较两个
    int
    慢一点。但是速度不够慢,不能保证用MD5/SHA1/等站在你的头上。这样做的所有开销都会比字符串的速度慢得多

    另一方面,如果您计划使用长度超过767字节的唯一字符串,则需要做一些事情。如果是这样,我将讨论一些解决办法

    同时,我认为SHA256是严重的过度使用。对于128位MD5,“在一个包含9万亿字符串的表中,9万亿次错误复制中有一次机会。”对于仅“数百万次”的情况,可能性更为渺茫

    还有一点<代码>二进制(20)
    CHAR(20)COLLATE…\u bin
    的处理方式相同。更复杂的排序需要更多的努力

    附录

    背景:实际上MySQL中唯一的索引类型是BTree。这是一个有序的列表。它对于“点查询”(给定一个键,查找记录)也非常有效。对于InnoDB,BTree块是16KB的块。对于一百万行来说,BTree大约有3层深。一万亿——六

    SHA256/UUID/GUID/其他摘要——当您有非常多的行时,所有这些都会执行得非常糟糕。这是因为它们是非常随机的,在非常大的表的情况下,您不可能将所需的块缓存在RAM中

    以下是一个折衷的解决方案,以人工表为例:

    CREATE TABLE foo (
        in_reply_to VARCHAR(500) NOT NULL CHARACTER SET ...,
        ...
        KEY(in_reply_to(20))
        PRIMARY KEY (...)
    ) ENGINE=InnoDB;
    
    SELECT ... FROM foo
        WHERE in_reply_to = '...';
    
    请注意,我没有sha256或其他摘要

    KEY(在回复(20)中)
    用该字段的前20个字符建立索引。
    UNIQUE(在对(20)的回复中)
    也可以这样做,但也可以是const