web-dev-qa-db-ja.com

結合でクエリを高速化する方法

結合する必要があるテーブルがいくつかあります。 employeesテーブル(〜40万行)、companiesテーブル(〜1000万行)、および誰かが働いている場所を格納するemployee_companiesテーブルがあります。 。

基本的には、ある条件に一致するすべての従業員を取得する必要があります(彼らはウェブサイトを持っている会社で働いている、特定の国にいるなど)。これを取得するためにクエリを実行しましたが、時間がかかりすぎています。私はそれをスピードアップする必要があります。

SELECT  DISTINCT "employees".* 
FROM "employees" 
INNER JOIN "employee_companies" ON "employee_companies"."employee_id" = "employees"."id" 
INNER JOIN "companies" ON "companies"."id" = "employee_companies"."company_id" 
WHERE (employee_companies.employee_id IS NOT NULL)
AND (companies.website IS NOT NULL) 
AND (employees.country = 'Uruguay') 
ORDER BY employees.connections DESC

これはそのクエリの計画です:

Unique  (cost=877170.24..880752.72 rows=62304 width=1064) (actual time=24023.736..26001.876 rows=73318 loops=1)
  ->  Sort  (cost=877170.24..877326.00 rows=62304 width=1064) (actual time=24023.733..24305.989 rows=77579 loops=1)
        Sort Key: employees.connections DESC, employees.id, employees.name, employees.link, employees.role, employees.area, employees.profile_picture, employees.summary, employees.current_companies, employees.previous_companies, employees.skills, employees.education, employees.languages, employees.volunteer, employees.groups, employees.interests, employees.search_vector, employees.secondary_search_vector, employees.email_status, employees.languages_count, employees.role_hierarchy
        Sort Method: external merge  Disk: 85816kB
        ->  Nested Loop  (cost=2642.38..843246.15 rows=62304 width=1064) (actual time=139.870..23056.234 rows=77579 loops=1)
              ->  Hash Join  (cost=2641.95..221744.50 rows=77860 width=1068) (actual time=139.841..22617.587 rows=77579 loops=1)
                    Hash Cond: (employees.id = employee_companies.employee_id)
                    ->  Seq Scan on employees  (cost=0.00..212178.88 rows=409672 width=1064) (actual time=8.145..22369.166 rows=393725 loops=1)
                          Filter: ((country)::text = 'Uruguay'::text)
                          Rows Removed by Filter: 1075
                    ->  Hash  (cost=1666.42..1666.42 rows=78042 width=8) (actual time=44.675..44.675 rows=78042 loops=1)
                          Buckets: 131072  Batches: 1  Memory Usage: 4073kB
                          ->  Seq Scan on employee_companies  (cost=0.00..1666.42 rows=78042 width=8) (actual time=0.007..22.901 rows=78042 loops=1)
                                Filter: (employee_id IS NOT NULL)
              ->  Index Scan using companies_pkey on companies  (cost=0.43..7.97 rows=1 width=4) (actual time=0.004..0.004 rows=1 loops=77579)
                    Index Cond: (id = employee_companies.company_id)
                    Filter: (website IS NOT NULL)
Planning time: 1.957 ms
Execution time: 26025.045 ms

そして、これらは私が私のテーブルに持っている関連するインデックスです:

従業員

"employees_pkey" PRIMARY KEY, btree (id)
"ix_employees_country" btree (country)

会社

"companies_pkey" PRIMARY KEY, btree (id)
"empty_websites" btree (website) WHERE website IS NULL
"index_companies_on_website" btree (website)
"not_empty_websites" btree (website) WHERE website IS NOT NULL

employee_companies

"employee_companies_pkey" PRIMARY KEY, btree (id)
"index_employee_companies_on_company_id" btree (company_id)
"index_employee_companies_on_employee_id" btree (employee_id)
"index_employee_companies_on_employee_id_and_company_id" btree (employee_id, company_id)
"not_empty_employee_id" btree (employee_id) WHERE employee_id IS NOT NULL

私がやりたいことを行うための、より効率的でパフォーマンスの良い他の方法はありますか?

ありがとう!

4
jpbalarini

いくつかの推測シミュレーションに基づいて、クエリをわずかに改善できると思います。

  1. 外側のDISTINCT句を避けます(ただし、暗黙的にDISTINCTが存在します)。
  2. JOINに必要なデータが少なくなるように、データの一部を副選択する。

クエリは次のとおりです。

SELECT  
    employees.* 
FROM 
    employees 
WHERE
    employee_id IN
    (SELECT 
        -- Choose all employees from companies with website
        employee_id 
     FROM 
        employee_companies
        JOIN companies ON companies.company_id = employee_companies.company_id
     WHERE
        companies.website IS NOT NULL
    )
    -- Now filter only employees from 'Germany'
    AND employees.country = 'Germany' 
ORDER BY 
    employees.connections DESC ;

シミュレーションの生成に使用されるデータは次のとおりです。

テーブルとインデックスの定義:

CREATE TABLE employees
(
    employee_id integer PRIMARY KEY,
    country text,
    connections integer,
    something_else text
) ;

CREATE INDEX idx_employee_country 
   ON employees (country) ;

CREATE TABLE companies
(
    company_id integer PRIMARY KEY,
    website text,
    something_else text
) ;

CREATE INDEX not_empty_websites 
    ON companies(company_id, website) WHERE website IS NOT NULL ;

CREATE TABLE employee_companies
(
    employee_id integer NOT NULL REFERENCES employees(employee_id),
    company_id integer NOT NULL REFERENCES companies(company_id),
    PRIMARY KEY (employee_id, company_id)
) ;

CREATE INDEX company_employee
    ON employee_companies(company_id, employee_id) ;

1.000.000社(1000万に変更しても大きな違いはありません)。 90%はウェブサイトを持っていると思います。

INSERT INTO 
   companies
   (company_id, website)
SELECT
   generate_series(1, 1000000), 
   CASE WHEN random() > 0.1 THEN 'web.com' END AS website ;

8万人の従業員(約10%がドイツ人)

INSERT INTO
   employees 
   (employee_id, country, connections)
SELECT
    generate_series(1, 80000),
    case (random()*10)::integer
    when 0 then 'Germany'
    when 1 then 'United Kingdon'
    when 2 then 'United States'
    else 'Angola'
    end AS country,
    (random()*10)::integer AS connections ;

20万人の従業員x企業(これは、人々が平均で約3社で働いたことを意味します):

INSERT INTO 
    employee_companies
    (employee_id, company_id)
SELECT DISTINCT
    (random()*79999)::integer + 1,
    (random()*999999)::integer + 1
FROM
    generate_series (1, 200000) ;

dbfiddle hereで、このシミュレーションの縮小版を確認できます。このシミュレートされたデータがシナリオに十分に類似している場合、クエリを変更すると、サーバーの実行時間に関して3倍の改善が行われます。ぜひ試してみてください。


データをシミュレート(25分の1に縮小)した場合、実際のシナリオに近いシナリオでは、パフォーマンスはそれほど向上しません。それでも、1.5倍向上します。

これを確認してくださいdbfiddle

4
joanolo