Wydajność TSQL - DOŁĄCZ od wartości MIĘDZY min. I maks

10

Mam dwa stoliki, w których przechowuję:

  • zakres adresów IP - tabela przeglądowa kraju
  • lista żądań pochodzących z różnych adresów IP

Adresy IP były przechowywane jako bigints, aby poprawić wydajność wyszukiwania.

Oto struktura tabeli:

create table [dbo].[ip2country](
    [begin_ip] [varchar](15) NOT NULL,
    [end_ip] [varchar](15) NOT NULL,
    [begin_num] [bigint] NOT NULL,
    [end_num] [bigint] NOT NULL,
    [IDCountry] [int] NULL,
    constraint [PK_ip2country] PRIMARY KEY CLUSTERED 
    (
        [begin_num] ASC,
        [end_num] ASC
    )
)

create table Request(
    Id int identity primary key, 
    [Date] datetime, 
    IP bigint, 
    CategoryId int
)

Chcę uzyskać podział żądań według kraju, dlatego wykonuję następujące zapytanie:

select 
    ic.IDCountry,
    count(r.Id) as CountryCount
from Request r
left join ip2country ic 
  on r.IP between ic.begin_num and ic.end_num
where r.CategoryId = 1
group by ic.IDCountry

Mam wiele rekordów w tabelach: około 200 000 w IP2Countryi kilka milionów w Request, więc zapytanie zajmuje trochę czasu.

Patrząc na plan wykonania, najdroższą częścią jest Wyszukiwanie klastrowane na indeksie PK_IP2Country, które jest wykonywane wielokrotnie (liczba wierszy w żądaniu).

Poza tym czuję się trochę dziwnie w tej left join ip2country ic on r.IP between ic.begin_num and ic.end_numczęści (nie wiem, czy jest lepszy sposób na przeprowadzenie wyszukiwania).

Struktura tabeli, niektóre przykładowe dane i zapytanie są dostępne w SQLFiddle: http://www.sqlfiddle.com/#!3/a463e/3 (niestety nie sądzę, żebym mógł wstawić wiele rekordów w celu odtworzenia problemu, ale to mam nadzieję, że daje pomysł).

Nie jestem (oczywiście) ekspertem od wydajności / optymalizacji SQL, więc moje pytanie brzmi: czy są jakieś oczywiste sposoby na ulepszenie tej struktury / zapytania pod względem wydajności, których mi brakuje?

Cristian Lupascu
źródło
2
Czy adres IP może być mapowany na wiele krajów? Jeśli nie, możesz zawęzić swoje PK do just begin_num. Muszę też dołączać A BETWEEN B AND Cdość często i jestem ciekawy, czy istnieje sposób na osiągnięcie tego bez żmudnych połączeń RBAR.
Jon of All Trades
1
To trochę nie na temat twojego pytania, ale rozważę utworzenie begin_ipi end_iputrwalenie kolumn obliczeniowych, aby zapobiec możliwości synchronizacji tekstu i liczb.
Jon of All Trades
@ w0lf: czy zakresy się pokrywają ip2country (begin_num, end_num)?
ypercubeᵀᴹ
@JonofAllTrades zwykle jeden adres IP powinien należeć do jednego kraju, więc myślę, że twój pomysł na zapytanie give me the first record that has a begin_num < ip in asc order of begin_num(popraw mnie, jeśli się mylę) może być poprawny i poprawić wydajność.
Cristian Lupascu
1
@ w0lf: Moje wrażenia są takie, że serwer zasadniczo robi to w takim przypadku, ponieważ najpierw skanuje begin_num, a następnie skanuje w end_numobrębie tego zestawu i znajduje tylko jeden rekord.
Jon of All Trades

Odpowiedzi:

3

Potrzebujesz dodatkowego indeksu. W twoim przykładzie Fiddle dodałem:

CREATE UNIQUE INDEX ix_IP ON Request(CategoryID, IP)

Który obejmuje tabelę żądań i pobiera wyszukiwanie indeksu zamiast skanowania indeksu klastrowego.

Zobacz, jak to poprawia, i daj mi znać. Zgaduję, że to trochę pomoże, ponieważ skanowanie tego indeksu nie jest na pewno tanie.

JNK
źródło
Nie wiem dlaczego, ale wyniki wydają się być inne (w SQLFiddle)
Cristian Lupascu
@ w0lf: są różne (probbaly), ponieważ oboje wstawiasz losowe dane do tabel.
ypercubeᵀᴹ
@ypercube na pewno to jest przyczyną. Zrobiłem ostatnio tak wiele rzeczy, że zapomniałem, że dane były przypadkowe. Przepraszam.
Cristian Lupascu,
2

Zawsze istnieje podejście oparte na brutalnej sile: możesz eksplodować swoją mapę IP. Dołącz tabelę liczb do istniejącej mapy, aby utworzyć jeden rekord dla każdego adresu IP. To tylko 267 tys. Rekordów opartych na twoich danych Fiddle, bez problemu.

CREATE TABLE IPLookup
  (
  IP  BIGINT PRIMARY KEY,
  CountryID  INT
  )
INSERT INTO IPLookup (IP, CountryID)
  SELECT
    N.Number, Existing.IDCountry
  FROM
    ip2country AS Existing
    INNER JOIN Numbers AS N ON N.Number BETWEEN Existing.begin_num AND Existing.end_num

Dzięki temu poszukiwania będą prostsze i, miejmy nadzieję, szybsze. Ma to sens tylko wtedy, gdy dokonasz stosunkowo niewielu aktualizacji ip2country.

Mam nadzieję, że ktoś inny ma lepsze rozwiązanie!

Jon of All Trades
źródło
Cały zestaw danych wygenerowałby ponad 5 miliardów rekordów, więc nie sądzę, żebym to zrobił. Niemniej jednak jest to niezły pomysł; Jestem pewien, że jest to wykonalne w wielu podobnych przypadkach. +1
Cristian Lupascu,
0

Spróbuj tego:

SELECT ic.IDCountry,
        COUNT(r.Id) AS CountryCount
FROM Request r
INNER JOIN (SELECT begin_num+NUMS.N [IP], IDCountry 
            FROM ip2country
            CROSS JOIN (SELECT TOP(SELECT ABS(MAX(end_num-begin_num)) FROM ip2country) ROW_NUMBER() OVER(ORDER BY sc.name)-1 [N]
                        FROM sys.columns sc) NUMS
            WHERE begin_num+NUMS.N <= end_num) ic
ON r.IP = ic.IP
WHERE r.CategoryId = 1
GROUP BY ic.IDCountry
Vince Pergolizzi
źródło
dzięki, próbowałem twojego podejścia, ale wydaje się, że jest ono droższe niż wstępne zapytanie
Cristian Lupascu
Ile rzędów masz w każdej tabeli? Chciałbym odtworzyć skalę twojego problemu na moim DB i spróbować rozwiązać bez dodawania indeksu :)
Vince Pergolizzi
około 200 000 w IP2Country i kilka milionów (prawdopodobnie dziesiątki milionów w najbliższej przyszłości) w Request. Myślę, że jeśli rozwiążesz go bez indeksów, zasługujesz na tytuł „DBA roku” :)
Cristian Lupascu