Создание функции для автоматизации SQL-соединений - PullRequest
2 голосов
/ 10 мая 2019

Я работаю с базой данных из 60+ таблиц и пытаюсь написать функцию python, которая принимает таблицы, с которыми вы хотите работать, и выводит операторы объединения, которые вы должны использовать для соединения этих таблиц. столы. Я поместил все 60 таблиц в упорядоченный словарь, как показано ниже, в котором перечислены имя таблицы, первичный ключ и внешние ключи в каждой таблице.

OrderedDict({
    'table1_name':{'pk':'id', 'fk':{'table2_name':'table2_id', 'table3_name':'table3_id'}}
    'table2_name':{'pk':'id'},
    'table3_name':{'pk':'id', 'fk':{'table1_name':'table1_id'}
}) #Etc...

Я начал писать функцию с небольшим успехом, поскольку внешние ключи переплетаются друг с другом, что делает очень сложным переход от таблицы к таблице и поиск кратчайшего пути. Моя попытка написать функцию для этого выглядела примерно так:

def join_creator(main_table, *tables):    
    #Test if we can join other tables directly to main
     try:
        main_table
        main_pk = table_dict[main_table]["pk"]
     except:
        print('No primary key, this cannot be your main table')
        return

     result = f'FROM "public"."{main_table}" \n'
     for table in other_tables:
        try: 
            fk = table_dict[table]['fk'][main_table]
            result += f'LEFT JOIN "public"."{table}" ON {main_table}.{main_pk}={table}.{fk}\n'
        except KeyError:
            pass
     print(result)

Подводя итог, можно сказать, что ввод этой функции join_creator('table_1', 'table_2', 'table_3')

и вывод будет такой строкой:

FROM table_1
LEFT JOIN table_2 ON table_1.id = table_2.t1_id
LEFT JOIN table_3 ON table_1 = table_3.t1_id

Будем весьма благодарны за любые предложения о том, как этого добиться на высоком уровне!

1 Ответ

2 голосов
/ 10 мая 2019

Перефразируя, у вас есть ориентированный граф , узлами которого являются таблицы, а направленными ребрами являются внешние ключи. Если я правильно интерпретирую ваше значение, вы хотите найти наименьший набор ребер, который содержит пути от начального узла (основной таблицы) к каждому из узлов, которые вы даете.

Следует спросить, является ли ваш граф ацикличным (это означает, что не существует цикла внешних ключей, все ссылающиеся друг на друга; дочерний элемент дочернего элемента ... дочерний элемент таблицы никогда не является исходной таблицей). Если у вас нет циклов, могут быть сделаны некоторые упрощения, но ради аргумента предположим, что вы не знаете, существуют ли циклы.

С другой стороны, я буду предполагать, что любой произвольный действительный порядок соединения действительно то, что вы хотите (будьте осторожны - это довольно предположение для вашей модели данных!)

Один алгоритм, который вы можете использовать, - это поиск в ширину (bfs) , чтобы получить кратчайший путь от основной таблицы к каждому дочернему элементу. Это не гарантируется как оптимальное (оно жадно выбирает кратчайший путь для каждого узла назначения, когда объединение путей может быть проще). Тем не менее, это, безусловно, хорошая догадка для оптимального, и она справляется с возможностью циклов.

Как только вы выяснили все эти пути, у вас есть дерево, укорененное в главной таблице, и вы хотите добавить в таблицы оператор, сначала родители.

Ниже приведен некоторый поспешный код Python, который я написал для этого. Не стесняйтесь рефакторинг или комментарий для ясности.

from collections import deque


def join_sql(schema, main_table, *tables):
    # for each table, which tables does it connect to?
    children_map = {table:set() for table in schema}
    for child, properties in schema.items():
        parents = properties['fk']
        for parent in parents:
            children_map[parent].add(child)

    # What are all the tables in consideration?
    nodes = set(schema.keys())

    # get a tree of parent tables via breadth-first search.
    parent_tree = bfs(nodes, children_map, main_table)

    # Create a topological ordering on the graph;
    # order so that parent is joined before child.
    join_order = []
    used = {main_table}
    def add_to_join_order(t):
        if t in used or t is None:
            return
        parent = parent_tree.get(t, None)
        add_to_join_order(parent)
        join_order.append(t)
        used.add(t)

    for table in tables:
        add_to_join_order(table)

    lines = [f"FROM {main_table}"]
    for fk_table in join_order:
        parent_table = parent_tree[fk_table]
        parent_col = schema[parent_table]['pk']
        fk_col = schema[fk_table]['fk'][parent_table]
        lines.append(f'INNER JOIN {fk_table} ON {fk_table}.{fk_col} = {parent_table}.{parent_col}')
    return "\n".join(lines)


def bfs(nodes, children, start):
    parent = {}
    q = deque([start])

    while q:
        v = q.popleft()
        for w in children[v]:
            if w not in parent:
                parent[w] = v
                q.append(w)

    return parent



if __name__ == "__main__":
    schema = {'table1_name': {'pk': 'id', 'fk': {'table2_name': 'table2_id', 'table3_name': 'table3_id'}},
              'table2_name': {'pk': 'id', 'fk': {}},
              'table3_name': {'pk': 'id', 'fk': {'table1_name': 'table1_id'}},
              'table4_name': {'pk': 'id', 'fk': {'table3_name': 'table3_id'}},
              'table5_name': {'pk': 'id', 'fk': {'table3_name': 'table3_id'}},
              }
    print(join_sql(schema, 'table2_name', 'table2_name', 'table3_name', 'table4_name', 'table5_name'))


# FROM table2_name
# INNER JOIN table1_name ON table1_name.table2_id = table2_name.id
# INNER JOIN table3_name ON table3_name.table1_id = table1_name.id
# INNER JOIN table4_name ON table4_name.table3_id = table3_name.id
# INNER JOIN table5_name ON table5_name.table3_id = table3_name.id
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...