データガバナンスにおけるPyODPSの正しい使用

データガバナンスにおけるPyODPSの正しい使用

概要:テーブルの飽和(フィールドが空かどうか)、フィールドのしきい値(数値フィールドの値が有効な境界を超えているかどうか)は、単一のテーブルのフィールドレベルでのチェックサム統計であるため、データ品質を評価するための重要な指標です。そして、ほとんどすべてのTableは、スコープが広く、ロジックが単純で、反復性が高く、Python開発の効率が高いため、多くのデータエンジニアはPyODPSを使用して関連する関数を開発します。この記事では、PyODPSに基づいて、3つの方法を使用して「飽和統計」機能を実装し、それらの実行効率を示し、その理由を分析します。

結論:1。データの量が非常に少ない場合を除いて、ローカル処理にデータをプルすることは避けてください。2。SQLを実行する方法が最も効率的で直感的です。飽和状態のシナリオのみの場合は、この方法をお勧めしますが、 SQL構文の影響を受けます。制限、十分な柔軟性がありません。3。DataFrame SDKの効率はわずかに低くなりますが、使用方法は非常に柔軟であり、共通の処理ロジックを関数にカプセル化でき、コードの再利用率が高くなります。

テスト環境:Mac Book Pro | 4C / 16G / 512G

1.open_readerおよびパーティションレベルによる同時実装

テストテーブルのデータ量 パーティションの数 営業時間
332万 5 22分40秒

分析:テーブル内のデータは、検査と統計のためにopen_readerを介してローカルにプルされます。コードではマルチスレッドが使用されますが、「実際の」同時実行性はありません。1。ODPSの計算能力は使用されませんが、ローカル計算が使用されます。機能;2。PythonのGIL(Global Interpreter Lock)により、スレッド間のシリアル実行が可能になります。データ量が非常に少ない場合、この方法の利点は、ODPSインスタンスを作成する時間とリソースのオーバーヘッドを節約できることです。

2.execute_sql全表スキャンを介して実現します

テストテーブルのデータ量 パーティションの数 営業時間
60000 77 21秒
1,600万 23 18秒
3億1000万 14 50秒

分析:DataWorksでSQLを実行するのと同じです。SQLを綴ることができれば、目的の機能を実現できます。ただし、プロジェクトが全表スキャンを制限するように配置されている場合は、set odps.sql.allow.fullscan=true;操作する必要があります。欠点は、検証ロジックがSQLのスペル文字列によってスペルアウトされ、コードを再利用するのが難しいことです。

3.DataFrameによって実現

テストテーブルのデータ量 パーティションの数 営業時間
60000 77 53秒
1,600万 23 39秒
3億1000万 14 5分56秒

分析:DataFrameのMapReduce APIと集計関数sumを使用して、DataFrameはインスタンスを送信し、DateFrame操作をUDF SQLに変換し、マッパーを介して元のテーブルデータをint型カウントに変換してからsum操作を実行します。DataFrameがSQL実行よりも効率が低い理由は、カスタムPython関数と組み込み関数のパフォーマンスの違いです。たとえば、作成者が合計ではなく自分で作成したレデューサーを使用した場合、効率は大幅に低下します。

コード

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import sys

reload(sys)
sys.setdefaultencoding("utf-8")

from odps import ODPS
import datetime
from odps import options
from datetime import datetime, timedelta
from time import sleep
from odps.df import DataFrame
from odps.df import output
from queue import Queue
import threading

o = ODPS('xxx', 'xxx', 'xxx', endpoint='xxx')

options.interactive = True  # 用到 print 需要打开
options.verbose = True      # 输出进度信息和错误信息


def mapper(row):
    ret = [1]
    for i in range(len(row)):
        ret.append(1 if row[i] is None or str(row[i]).isspace() or len(str(row[i])) <= 0 else 0)
    yield tuple(ret)

def data_frame(table_name):
    findnull = DataFrame(o.get_table(table_name))
    col_num = len(findnull.dtypes)
    output_types = ['int' for i in range(col_num + 1)]
    output_names = [findnull.dtypes.columns[i].name for i in range(col_num)]
    output_names.insert(0, 'total_cnt')
    table = findnull.map_reduce(mapper, mapper_output_names=output_names, mapper_output_types=output_types)
    print(table.sum())

def check_data_by_execute_sql(table_name):
    ta = o.get_table(table_name)
    data_count = {
    
    }
    table_count = 0
    sql_str = 'select \n'
    for col in ta.schema.columns:
        col_name = col.name
        sql_str += "sum(case when (%s is null) or (%s in ('','null','NULL')) or (trim(%s) = '') then 1 else 0 end) as %s_yx,\n" % (col_name, col_name, col_name, col_name)
    sql_str += "count(1) as total_cnt \nfrom %s " %(table_name)
    print(sql_str)
    with o.execute_sql(sql_str).open_reader() as rec:
        for r in rec:
            for col in ta.schema.columns:
                print("%s\t\t%d" % (col.name, r.get_by_name(col.name + '_yx')))
            print("%s\t\t%d" % ('total_cnt', r.get_by_name('total_cnt')))

def get_last_day():
    today = datetime.today()
    last_day = today + timedelta(days=-1)
    return last_day.strftime('%Y%m%d')

count_queue = Queue()
threads = []

def check_data_by_open_reader(table_name, pt):
    ta = o.get_table(table_name)
    data_count = {
    
    }
    print(table_name + "\t:\t" + str(pt) + " STARTED")
    rec = ta.open_reader(partition=str(pt))
    table_count = rec.count
    for r in rec:
        for col in ta.schema:
            col_value = r.get_by_name(col.name)
            if col.name not in data_count:
                data_count[col.name] = 0
            if col_value == None or str(col_value).isspace() or len(str(col_value)) <= 0:
                data_count[col.name] += 1
    count_queue.put((data_count, table_count))
    print(table_name + "\t:\t" + str(pt) + " DONE")

# 假设 dt 为分区字段
def check_data(table_name):
    table_tocheck = o.get_table(table_name)
    for pt in table_tocheck.iterate_partitions("dt='" + get_last_day() + "'"):  
        t = threading.Thread(target=check_data_by_open_reader, args=(table_name, pt))
        t.setDaemon(True)
        t.start()
        threads.append(t)

    print("线程数共:" + str(len(threads)))

    while True:
        thread_num = len(threading.enumerate()) - 1
        print("线程数量是%d" % thread_num)
        if thread_num <= 0:
            break
        sleep(10)

    total_cnt = 0
    total_data_cnt = {
    
    }
    while not count_queue.empty():
        pt_data = count_queue.get()
        data_count = pt_data[0]
        total_cnt += pt_data[1]
        for col_name in data_count.keys():
            if col_name not in total_data_cnt:
                total_data_cnt[col_name] = 0
            total_data_cnt[col_name] += data_count[col_name]

    print(total_cnt, total_data_cnt)

if __name__ == '__main__':
    table_name = 'xxxx' 

    if len(sys.argv) == 2:
        if sys.argv[1] not in ('1', '2', '3'):
            print("ARG ERROR: %s 1|2|3" % sys.argv[0])
            exit()
        print(datetime.now().strftime('%Y-%m-%d %H:%M:%S  BEGIN with ' + table_name))
        if sys.argv[1] == '1':
            check_data(table_name)
        elif sys.argv[1] == '2':
            check_data_by_execute_sql(table_name)
        elif sys.argv[1] == '3':
            data_frame(table_name)
        print(datetime.now().strftime('%Y-%m-%d %H:%M:%S  DONE with ' + table_name))
    else:
        print("ARG ERROR: %s 1|2|3" % sys.argv[0])

おすすめ

転載: blog.csdn.net/ManWZD/article/details/106882005