Por exemplo, você pode imediatamente mostrar ao gerente de qual cidade está vindo a chamada, restringir a lista de preços e condições de entrega atuais, exibir o cartão do chamador, as últimas transações com ele, um contato específico, ... - e muitas coisas úteis, como nosso VLSI CRM pode fazer !

Como implementar essa funcionalidade sozinho? Acontece que não é tão difícil. Você pode literalmente construir e testar um modelo funcional - você só precisa de um pacote de Node.js e PostgreSQL.
Determine a região por número
Vamos supor que o PABX nos envie um número de telefone de entrada, já normalizado e formatado em até 10 dígitos (consideraremos apenas chamadas dentro da Rússia). Qual é a maneira mais eficiente de entender de onde veio a chamada?
Coletando códigos de telefone
Primeiro, precisamos de um banco de dados de códigos telefônicos da Rússia em relação às regiões. Para fazer isso, você pode usar uma fonte oficial - um extrato atualizado do plano de numeração no site da Agência Federal de Comunicações.
Mas encontrar não é suficiente, você precisa baixar e extrair esses dados. Um pequeno script para Node.js usando a biblioteca de solicitações nos ajudará com isso :
const async = require('async')
, request = require('request');
const fs = require('fs');
let queue = [
'ABC-3xx'
, 'ABC-4xx'
, 'ABC-8xx'
, 'DEF-9xx'
]
.map(key => (
{
base : 'https://rossvyaz.gov.ru'
, path : `/data/${key}.csv`
}
));
let ranges = [];
async.doWhilst(
cb => {
//
let task = queue.shift();
request(
{
url : task.base + task.path
, pool : false
}
, (err, res, body) => {
// CSV
body.split('\n').forEach(line => {
let tds = line.split(';');
let place = tds[5].split('|');
ranges.push([
tds[0]
, tds[1]
, tds[2]
, tds[4]
, place[place.length - 1]
, place[place.length - 2] && place[place.length - 2].startsWith('-') ? place[place.length - 2] : ''
, place.length > 1
? place[0].startsWith('-')
? ''
: place[0]
: ''
]);
});
return cb(err);
}
);
}
// ,
, cb => {
return cb(null, queue.length);
}
// -
, err => {
//
ranges.forEach(row => {
//
let ln = row[0].length + row[1].length - 10;
if (ln > 0) {
let sfx = row[0].slice(-ln);
if (row[1].startsWith(sfx) && row[2].startsWith(sfx)) {
row[1] = row[1].slice(ln);
row[2] = row[2].slice(ln);
}
}
//
let pfx;
for (let i = 1; i < row[1].length; i++) {
if (row[2].startsWith(row[1].slice(0, i))) {
pfx = row[1].slice(0, i);
}
else {
break;
}
}
if (pfx) {
row[0] = row[0] + pfx;
row[1] = row[1].slice(pfx.length);
row[2] = row[2].slice(pfx.length);
}
});
let sql = `
SET client_encoding = 'UTF-8';
CREATE TABLE phonecodes(
code
varchar
, numb
varchar
, nume
varchar
, oper
varchar
, region
varchar
, district
varchar
, city
varchar
);
COPY phonecodes FROM STDIN;
`;
// COPY-
let copy = ranges.map(row => row.join('\t')).join('\n') + '\n\\.\n';
fs.writeFileSync('phonecodes.sql', sql + copy);
}
);
Agora vamos carregá-lo em nossa base de teste, e você pode trabalhar:
psql -f phonecodes.sql -U postgres tst
Se tudo correr como deveria, quase 378 mil faixas serão carregadas em nossa mesa:
SET
CREATE TABLE
COPY 377937
Observe que, em nosso exemplo, tanto o código quanto os números de limite do intervalo são representados por strings. Sim, eles podem ser transformados integer/bigint
, mas não faremos isso por enquanto. Além disso, o número de telefone recebido nem sempre consiste apenas em números - por exemplo, alguns telefones públicos podem informar seu número com o "dígito A".
"Eles estão procurando bombeiros, a polícia está procurando ..."
Vamos tentar uma pergunta ingênua primeiro:
WITH src AS (
SELECT '4852262000' num --
)
SELECT
*
FROM
src
, phonecodes
WHERE
num LIKE (code || '%') AND --
num BETWEEN (code || numb) AND (code || nume) --
LIMIT 1;

[veja explain.tensor.ru]
Quase 70 mil linhas foram subtraídas (e foi uma sorte que nem todas as 380 linhas!), quase 10 MB de dados foram retirados ... não muito eficientemente, mas o resultado foi alcançado:
num | code | numb | nume | oper | region | district | city
-----------------------------------------------------------------------------------
4852262000 | 485226 | 0000 | 9999 | | . | |
Mas vamos nos livrar disso de alguma forma
Seq Scan
! Para fazer isso, precisamos apenas de um índice que ajudará a pesquisar por LIKE
, certo? ..
Infelizmente, não. Se precisarmos pesquisar
column LIKE (val || '%')
, os índices de prefixo com varchar_pattern_ops nos ajudarão , mas temos o oposto - val LIKE (column || '%')
. E temos uma situação próxima à que descrevi no artigo "Classificando erros de logs do PostgreSQL" .
Usamos o conhecimento da área aplicada
Perto, mas, felizmente, ainda é muito mais simples - nossos dados são fixos e há relativamente poucos deles. Além disso, os registros são distribuídos de forma bastante esparsa por códigos:
SELECT -- -
ranges
, count(*)
FROM
(
SELECT --
code
, count(*) ranges
FROM
phonecodes
GROUP BY
1
) T
GROUP BY
1
ORDER BY
1 DESC;
Apenas cerca de cem códigos têm 10 intervalos e quase um quarto tem exatamente um:
ranges | count
--------------
10 | 121
9 | 577
8 | 1705
7 | 3556
6 | 6667
5 | 10496
4 | 12491
3 | 20283
2 | 22627
1 | 84453
Portanto, vamos apenas indexar o código por enquanto. E como precisamos de todos os intervalos do mesmo código todos juntos, ordenaremos nossa tabela com a ajuda de de
CLUSTER
modo que os registros fiquem fisicamente próximos uns dos outros:
CREATE INDEX ON phonecodes(code);
CLUSTER phonecodes USING phonecodes_code_idx;
Agora, vamos lembrar que nosso número de telefone consiste em exatamente (no total!) 10 dígitos, entre os quais precisamos isolar o código do prefixo. Ou seja, nossa tarefa é resolvida com calma por uma simples enumeração de não mais do que 10 opções:
WITH RECURSIVE src AS (
SELECT '4852262000' num
)
, T AS (
SELECT
num pfx -- ""
, NULL::phonecodes pc
FROM
src
UNION ALL
SELECT
substr(pfx, 1, length(pfx) - 1) -- ""
, (
SELECT
X
FROM
phonecodes X
WHERE
code = T.pfx AND --
(TABLE src) BETWEEN (code || numb) AND (code || nume) --
LIMIT 1
) pc
FROM
T
WHERE
pc IS NOT DISTINCT FROM NULL AND -- ,
length(pfx) > 2 -- ...
)
SELECT
(pc).* -- ""
FROM
T
WHERE
pc IS DISTINCT FROM NULL;

[veja explain.tensor.ru]
Levamos apenas 5 chamadas de índice para encontrar o código que estávamos procurando. O ganho parece microscópico em números absolutos, mas obtivemos uma redução de 150 vezes na carga em relação à opção ingênua! Se o seu sistema tiver que processar dezenas ou centenas de milhares de solicitações por hora, a economia será muito grande!
E você pode fazer ainda menos iterações sobre o índice - se todos os códigos forem preliminarmente reduzidos à forma clássica "de 3 para 5 dígitos". No entanto, o número de intervalos em cada código aumentará e filtrá-los pode adicionar problemas.
int8range + GiST
Como corretamente observado nos comentários Miksir, uma vez que temos todos os pares "código + intervalo" e o número de entrada tem estritamente a mesma dimensão de 10 dígitos, então o problema pode ser reduzido a uma busca de intervalo entre valores numéricos.
Para fazer isso, criaremos um índice que tratará nossos registros como
int8range
:
CREATE INDEX ON phonecodes USING gist(
int8range(
(code || numb)::bigint --
, (code || nume)::bigint --
, '[]' --
)
);
Depois disso, podemos usá-lo na solicitação:
WITH src AS (
SELECT '4852262000'::bigint num
)
SELECT
*
FROM
phonecodes
WHERE
int8range((code || numb)::bigint, (code || nume)::bigint, '[]') @> ( --
SELECT
int8range(num, num, '[]') -- ""
FROM
src
)
LIMIT 1;

[veja explain.tensor.ru]
Intervalos não sobrepostos + árvore b
Primeiro, vamos garantir que nossos intervalos numéricos não se sobreponham realmente:
SELECT
*
FROM
phonecodes X
, phonecodes Y
WHERE
int8range((X.code || X.numb)::bigint, (X.code || X.nume)::bigint, '[]') &&
int8range((Y.code || Y.numb)::bigint, (Y.code || Y.nume)::bigint, '[]') AND
X.ctid <> Y.ctid;
Se você obtiver "nada" - está tudo bem, e você pode aplicar a seguinte otimização: o número só pode ser incluído no intervalo, ao final (ou início) do qual está mais próximo .
Para encontrar o "começo" mais próximo, precisamos apenas de um índice btree regular:
CREATE INDEX ON phonecodes((code || numb));
WITH src AS (
SELECT '4852262000' num
)
SELECT
*
FROM
src
, LATERAL (
SELECT
*
FROM
( --
SELECT
*
FROM
phonecodes
WHERE
(code || numb) <= src.num
ORDER BY
(code || numb) DESC
LIMIT 1
) T
WHERE
src.num BETWEEN (code || numb) AND (code || nume) --
) T;
Apesar de sua aparente simplicidade, esta opção tem um desempenho pior do que a anterior:

[veja explain.tensor.ru]
Identificamos o cliente por número
Agora vamos imaginar que já temos uma tabela com clientes, onde o número de telefone "limpo" está escrito - todos os colchetes, hífens, etc. são removidos.
Mas aqui está o incômodo, nem todos eles têm um código de cidade - ou os gerentes têm preguiça de pontuar ou o PBX está configurado de modo que não envia números completos, mas "intracity" ... Como então encontrar um cliente - afinal, uma busca de correspondência completa não funcionará mais?
PBX fornece o número completo
Nesse caso, usaremos o mesmo algoritmo "exaustivo" . Apenas "cortaremos" os números não do final do número, mas do início.
Se o número no cartão do cliente foi indicado por completo, vamos topar com ele na primeira iteração. Se não completamente - quando "cortamos" alguns dos códigos apropriados.
Claro, precisaremos de algum tipo de verificação cruzada com outros detalhes (endereço, TIN, ...) para que não tenhamos uma situação de "cortar" o código de Moscou do número recebido e encontrar um cliente de São Petersburgo pelo número de 7 dígitos restantes. Petersburgo.
PBX fornece um número de "cidade"
: 262000
: 4852262000
Aqui a situação é mais interessante. Não podemos "incrementar" todos os códigos possíveis a um número curto e tentar pesquisar - há muitos deles. Vejamos a situação do outro lado - literalmente:
reverse(262000) -> 000262
reverse(4852262000) -> 0002622584
Acontece que se você expandir as linhas com números, a tarefa se transforma em uma pesquisa de prefixo regular , que é facilmente resolvida usando um índice com varchar_pattern_ops e
LIKE
!
CREATE INDEX ON client(reverse(phone) varchar_pattern_ops);
SELECT
*
FROM
client
WHERE
reverse(phone) LIKE (reverse($1) || '%');
E então, novamente, verificamos as informações adicionais - de qual região o PBX nos enviou o número, a qual região o cliente pertence.