【问题标题】:MySQL - Indexing of stringsMySQL - 字符串索引
【发布时间】:2015-11-17 23:49:24
【问题描述】:

我正在寻找一种对字符串施加唯一约束的最佳方法。

我的用例是通过 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();
    }
}

MessageRepository.php

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 是通过重构位集来搜索的,并搜索所有应该完美利用放置在表上的唯一约束的列。

根据映射的身份搜索消息,按日期降序排列,仅提取一行。

【问题讨论】:

  • 所以你有一张桌子messages 什么的?显示结构有助于回答您的问题。
  • 你最终会发生碰撞。您会注意到,任何加密方法都有其自身的中度到极端性能下降。您是否表明使用 smtp ID 无法扩展?
  • 使字符串在数据库中唯一的问题通常可以通过您使用它的方式来解决 - 创建哈希,存储哈希,使其唯一。
  • @Drew 没关系,因为在将新消息匹配到另一个时,我只会选择最近匹配的。它是一个可用性工具。为了防止意外重复,我将使用消息 ID,加上其他一些字段来“猜测”它是否真的是唯一的。
  • 对不起,如果我误导了你 - 不,没有任何原生的东西,我只是想确认你所做的一切正常。您可以使用UNHEX() MySQL 方法将哈希存储在二进制列中,这样您就可以减少存储需求(不需要 varchars)。

标签: php mysql string performance indexing


【解决方案1】:

你正在解决一个不存在的问题。

当然比较两个字符串比比较两个INTs 慢一点。但速度还不够慢,不足以保证用 MD5/SHA1/等站在你的头上。与字符串相比,所有这些开销都会使事情变慢。

另一方面,如果您计划拥有一个长度超过 767 字节的唯一字符串,则需要做一些事情。如果是这样,我会讨论一些出路。

同时,我会争辩说 SHA256 太过分了。对于 128 位 MD5,“在 9 万亿个字符串的表中,有 9 万亿个错误重复的机会。”对于仅仅“数百万”来说,可能性就更小了。

另外一点...BINARY(20)CHAR(20) COLLATE ..._bin 的处理方式相同。更复杂的排序规则需要一些更多的努力。

附录

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

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(in_reply_to(20)) 使用该字段的前 20 个字符构建索引。
UNIQUE(in_reply_to(20)) 也会这样做,但也将 20 个字符限制为唯一。这不是你想要的。

这是我的SELECT 的工作原理:

  1. 查看该键,找到与前 20 个字符匹配的 1 或 2 行或几行。
  2. 验证整个in_reply_to与搜索字符串匹配——完全匹配
  3. 交付结果集。

最好选择“20”作为两者之间的良好折衷

  • 足够长,通常是独一无二的
  • 足够短以保持密钥的 BTree 较小。 (更小 --> 更多可缓存 --> 更快)。

您可能已经注意到缺少一件事——MySQL 没有进行唯一性检查;你的代码必须通过执行我的SELECT 的一些变体来做到这一点。

SHA256 有 256 位,所以它需要BINARY(32)。即使有 1e25 个不同的文档,仍有大约 1e25 中的 1 个可能出现错误的重复。

【讨论】:

  • 哈希算法不是我担心的部分。该部分可能需要一整秒钟才能使用它的次数。一旦 ID 被收集和转换,只有转换后的版本很重要,即 I.E.固定长度为 64 个字符的 Sha256 编码字符串。当开始输入数百万个这些时,就会出现问题。唯一性在这种情况下并不是很重要,它只需要非常不可能,所以如果降低哈希复杂度更好,那就这样吧。但是搜索速度是我真正需要 O(1) 次的。
  • 这就是我设置这些分区的原因,希望 MySql 将其视为树查找。我希望通过这个问题弄清楚,MySql 是否对索引字符串或二进制值执行这种分区。
  • 感谢您的更新。这是非常有用的。对我来说幸运的是,我可以一次实施多种方法,并在几个月内找出最好的方法。我将把这个例子添加到我的实现中。澄清一下,我需要丢失唯一索引,将原始数据添加到 varchar[500] 和长度为 20 的索引?再次感谢。
  • 是的,实施多种方法。在做的时候,看看你是否能弄清楚为什么一个人更适合你的情况。写一篇关于这一切的博客。是的,丢失UNIQUE 索引,拥有VARCHAR(something),并使用前缀20(或某个值)。
  • SELECT COUNT(DISTINCT LEFT(foo, 15)), COUNT(DISTINCT( LEFT(foo, 20), ... 会让您对使用多大的前缀有所了解。
猜你喜欢
  • 2014-11-06
  • 2011-09-23
  • 2018-01-09
  • 2011-08-13
  • 1970-01-01
  • 2017-01-27
  • 2021-07-20
  • 2010-10-27
  • 2015-04-23
相关资源
最近更新 更多