【问题标题】:What is the best way to implement a substring search in SQL?在 SQL 中实现子字符串搜索的最佳方法是什么?
【发布时间】:2011-03-20 05:46:08
【问题描述】:

这里有一个简单的 SQL 问题。在 varchar 列中,我们想在字段中的任何位置搜索字符串。实现此性能的最佳方法是什么?显然索引在这里没有帮助,还有其他技巧吗?

我们使用 MySQL 并且有大约 300 万条记录。我们需要每秒执行许多这样的查询,因此真正尝试以最佳性能实现这些查询。

到目前为止,最简单的方法是:

Select * from table where column like '%search%'

我应该进一步指定该列实际上是一个长字符串,例如“sadfasdfwerwe”,我必须在此列中搜索“asdf”。 所以它们不是句子,而是试图匹配其中的一个词。全文搜索在这里还有帮助吗?

【问题讨论】:

    标签: sql mysql full-text-search query-optimization


    【解决方案1】:

    查看我的演示文稿Practical Fulltext Search in MySQL

    我比较过:

    今天我要使用的是Apache Solr,它将 Lucene 放入一个具有许多额外功能和工具的服务中。


    您的评论:啊哈,好吧,不。我提到的全文搜索功能都无济于事,因为它们都假设某种单词边界

    另一种有效查找任意子字符串的方法是N-gram 方法。基本上,为所有可能的 N 个字母序列创建一个索引,并指向每个相应序列出现的字符串。这通常使用 N=3 或 trigram 来完成,因为这是匹配较长子字符串和将索引保持在可管理大小之间的折衷点。

    我不知道有任何 SQL 数据库透明地支持 N-gram 索引,但您可以使用倒排索引自行设置:

    create table trigrams (
      trigram char(3) primary key
    );
    
    create table trigram_matches (
      trigram char(3),
      document_id int,
      primary key (trigram, document_id),
      foreign key (trigram) references trigrams(trigram),
      foreign key (document_id) references mytable(document_id)
    );
    

    现在用困难的方式填充它:

    insert into trigram_matches
      select t.trigram, d.document_id
      from trigrams t join mytable d
        on d.textcolumn like concat('%', t.trigram, '%');
    

    当然,这需要相当长的时间!但是一旦完成,您可以更快地搜索:

    select d.*
    from mytable d join trigram_matches t
      on t.document_id = d.document_id
    where t.trigram = 'abc'
    

    当然,您可以搜索超过三个字符的模式,但倒排索引仍然有助于缩小搜索范围:

    select d.*
    from mytable d join trigram_matches t
      on t.document_id = d.document_id
    where t.trigram = 'abc'
      and d.textcolumn like '%abcdef%';
    

    【讨论】:

    • 我稍微重新编辑了这个问题,这仍然适用吗?
    • PostgreSQL 有 pg_trgm contrib 包,它引入了一种索引三元组的方法。 postgresql.org/docs/current/static/pgtrgm.html
    • 我错过了您填充三元表的位置。
    • @BillKarwin 是否愿意通过插入 trigrams 表进行更新?
    • 我将把它作为练习留给读者编写一个循环来生成三元组。恕我直言,如果你不知道怎么做,你就不应该玩数据结构。
    【解决方案2】:

    如果您想匹配整个单词,请查看FULLTEXT 索引和MATCH() AGAINST()。当然,还要为您的数据库服务器加载负载:根据您的特定需求将结果缓存一段适当的时间。

    【讨论】:

      【解决方案3】:

      首先,这可能是一个设计不佳的表的问题,该表将分隔字符串存储在一个字段中,而不是正确设计以制作相关表。如果是这种情况,您应该修复您的设计。

      如果您有一个包含长描述性文本的字段(例如注释字段)并且搜索总是按整个单词,您可以进行全文搜索。

      考虑是否可以要求您的用户至少为您提供他们正在搜索的内容的第一个字符,如果它是一个普通字段,例如 Last_name。

      考虑先进行完全匹配搜索,如果没有返回结果,则仅执行通配符匹配。如果您有可以提供完全匹配的用户,这将起作用。我们曾经用机场名称搜索做过一次,如果他们输入确切的名称,它会很快返回,如果不输入,则速度会慢一些。

      如果您只想搜索不是可能在文本中某处出现的单词的字符串,那么您几乎会陷入性能不佳的困境。

      【讨论】:

        【解决方案4】:
        1. 如果您的语言不是英语,mysql 全文搜索的质量(为此)很差

        2. 三元组搜索给出了非常好的结果,对于这个任务

        3. postgreSQL 有trigram index,很容易使用:)

        4. 但如果您需要在 mysql 中执行此操作,请尝试使用 Bill Karwin 答案的改进版本:

          -每个三元组只存储一次

          -一个简单的php类使用数据

          <?php
          
            /*
          
              # mysql table structure
              CREATE TABLE `trigram2content` (
          `trigram_id` int NOT NULL REFERENCES trigrams(id),
          `content_type_id` int(11) NOT NULL,
          `record_id` int(11) NOT NULL,
          PRIMARY KEY (`content_type_id`,`trigram_id`,`record_id`)
          );
          
          #each trigram is stored only once
          CREATE TABLE `trigrams` (
          `id` int not null auto_increment,
          `token` varchar(3) NOT NULL,
          PRIMARY KEY (id),
          UNIQUE token(token)
          ) DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
          
          
          SELECT count(*), record_id FROM trigrams t
          inner join trigram2content c ON t.id=c.trigram_id
          WHERE (
          t.token IN ('loc','ock','ck ','blo',' bl', ' bu', 'bur', 'urn')
          AND c.content_type_id = 0
          )
          GROUP by record_id
          ORDER BY count(*) DESC
          limit 20;
          
          
          */
          class trigram
          {
          
              private $dbLink;
          
              var $types = array(
                  array(0, 'name'),
                  array(1, 'city'));
          
          
              function trigram()
              {
                //connect to db
                $this->dbLink = mysql_connect("localhost", "username", "password");
                if ($this->dbLink) mysql_select_db("dbname");
                else mysql_error();
          
                mysql_query("SET NAMES utf8;", $this->dbLink);
              }
          
              function get_type_value($type_name){
                for($i=0; $i<count($this->types); $i++){
                    if($this->types[$i][1] == $type_name)
                        return $this->types[$i][0];
                }
                return "";
              }
          
              function getNgrams($word, $n = 3) {
                  $ngrams = array();
                  $len = mb_strlen($word, 'utf-8');
                  for($i = 0; $i < $len-($n-1); $i++) {
                      $ngrams[] = mysql_real_escape_string(mb_substr($word, $i, $n, 'utf-8'), $this->dbLink);
                  }
                  return $ngrams;
              }
          
              /**
              input: array('hel', 'ell', 'llo', 'lo ', 'o B', ' Be', 'Bel', 'ell', 'llo', 'lo ', 'o  ')
              output: array(1,     2,     3,      4,      5,      6,      7,     2,   3,  4,      8)
              */
              private function getTrigramIds(&$t){
                  $u = array_unique($t);
                  $q = "SELECT * FROM trigrams WHERE token IN ('" . implode("', '", $u) . "')";
          
                  $query = mysql_query($q, $this->dbLink);
                  $n = mysql_num_rows($query);
          
                  $ids = array(); //these trigrams are already in db, they have id
                  $ok = array();
          
                  for ($i=0; $i<$n; $i++)
                  {
                    $row = mysql_fetch_array($query, MYSQL_ASSOC);
                    $ok []= $row['token'];
                    $ids[ $row['token'] ] = $row['id'];
                  }
                  $diff = array_diff($u, $ok); //these trigrams are not yet in the db
                  foreach($diff as $n){
                      mysql_query("INSERT INTO trigrams (token) VALUES('$n')", $this->dbLink);
                      $ids[$n]= mysql_insert_id();
                  }
          
                  //so many ids than items (if a trigram occurs more times in input, then it will occur more times in output as well)
                  $result = array();
                  foreach($t as $n){
                      $result[]= $ids[$n];
                  }
                  return $result;
              }
          
              function insertData($id, $data, $type){
                  $t = $this->getNgrams($data);
          
                  $id = intval($id);
                  $type = $this->get_type_value($type);
                  $tIds = $this->getTrigramIds($t);
                  $q = "INSERT INTO trigram2content (trigram_id, content_type_id, record_id) VALUES ";
                  $rows = array();
                  foreach($tIds as $n => $tid){
                      $rows[]= "($tid, $type, $id)";
                  }
                  $q .= implode(", ", $rows);
                  mysql_query($q, $this->dbLink);
              }
          
              function updateData($id, $data, $type){
                  mysql_query("DELETE FROM trigram2content WHERE record_id=".intval($id)." AND content_type_id=".$this->get_type_value($type), $this->dbLink);
                  $this->insertData($id, $data, $type);
              }
          
              function search($str, $type){
          
                  $tri = $this->getNgrams($str);
                  $max = count($tri);
                  $q = "SELECT count(*), count(*)/$max as score, record_id FROM trigrams t inner join trigram2content c ON t.id=c.trigram_id
          WHERE (
          t.token IN ('" . implode("', '", $tri) . "')
          AND c.content_type_id = ".$this->get_type_value($type)."
          )
          GROUP by record_id
          HAVING score >= 0.6
          ORDER BY count(*) DESC
          limit 20;";
                  $query = mysql_query($q, $this->dbLink);
                  $n = mysql_num_rows($query);
          
                  $result = array();
                  for ($i=0; $i<$n; $i++)
                  {
                    $row = mysql_fetch_array($query, MYSQL_ASSOC);
                    $result[] = $row;
                  }
                  return $result;
              }
          
          
          };
          

        及用法:

         $t = new trigram();
        
         $t->insertData(1, "hello bello", "name");
         $t->insertData(2, "hellllo Mammmma mia", "name");
        
          print_r($t->search("helo", "name"));
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2010-12-14
          • 2014-08-27
          • 2013-08-22
          • 2013-05-25
          • 2016-05-23
          • 2016-01-19
          • 1970-01-01
          相关资源
          最近更新 更多